├── .prettierignore ├── .env.test ├── src ├── interfaces │ ├── validation │ │ └── sandbox.ts │ ├── types │ │ └── config.ts │ ├── actions │ │ └── v1 │ │ │ ├── getFaq.ts │ │ │ ├── getPublicServices.ts │ │ │ ├── getUserIdentifier.ts │ │ │ ├── unauthorizedSimulate.ts │ │ │ ├── testAcquirerProviderEncodedData.ts │ │ │ ├── bankIdCallback.ts │ │ │ ├── bankIdAuthCodeResult.ts │ │ │ ├── getAppVersion.ts │ │ │ ├── faq │ │ │ ├── createFaq.ts │ │ │ └── updateFaq.ts │ │ │ ├── getAppSettings.ts │ │ │ ├── errorTemplates │ │ │ ├── createErrorTemplate.ts │ │ │ ├── updateErrorTemplate.ts │ │ │ ├── getErrorTemplateByErrorCode.ts │ │ │ └── getErrorTemplatesList.ts │ │ │ ├── bumpStoreTags.ts │ │ │ └── testAcquirerProviderResponse.ts │ ├── services │ │ ├── documents.ts │ │ ├── version.ts │ │ ├── partner.ts │ │ ├── user.ts │ │ ├── settings.ts │ │ ├── publicServicesList.ts │ │ ├── faq.ts │ │ ├── errorTemplate.ts │ │ └── processData.ts │ ├── routes │ │ ├── mortgage.ts │ │ ├── marriage.ts │ │ ├── file.ts │ │ ├── index.ts │ │ ├── payment.ts │ │ ├── documentDelivery.ts │ │ ├── publicServiceCatalog.ts │ │ ├── depositGuaranteePayments.ts │ │ ├── documentAcquirers.ts │ │ ├── vehicleReRegistration.ts │ │ ├── gateway.ts │ │ ├── partner.ts │ │ ├── notification.ts │ │ ├── publicService.ts │ │ └── appRoute.ts │ ├── externalEventListeners │ │ ├── notification │ │ │ └── index.ts │ │ └── index.ts │ ├── models │ │ ├── errorTemplate.ts │ │ ├── cms │ │ │ └── faq.ts │ │ └── faqCategory.ts │ ├── providers │ │ └── index.ts │ ├── middlewares │ │ └── index.ts │ ├── config.ts │ ├── application.ts │ ├── queue.ts │ └── profileFeature │ │ └── index.ts ├── providers │ └── faq │ │ ├── index.ts │ │ └── strapiFaqProvider.ts ├── index.ts ├── utils │ ├── transformers.ts │ ├── tracking.ts │ └── index.ts ├── dataMappers │ ├── publicServiceDataMapper.ts │ ├── errorTemplateDataMapper.ts │ └── cms │ │ ├── faqCategoryCmsDataMapper.ts │ │ └── faqCmsDataMapper.ts ├── routes │ ├── verifier.ts │ ├── defaults.ts │ ├── documents │ │ ├── user.ts │ │ ├── passports.ts │ │ ├── taxpayerCard.ts │ │ ├── foreignPassport.ts │ │ ├── internalPassport.ts │ │ ├── serviceEntrance.ts │ │ └── driverLicense.ts │ ├── feed.ts │ ├── support.ts │ ├── diiaSignature.ts │ ├── documentDelivery.ts │ ├── analytics.ts │ └── conference.ts ├── models │ ├── errorTemplate.ts │ └── faqCategory.ts ├── middlewares │ ├── redirect.ts │ ├── external.ts │ └── multipart.ts ├── actions │ └── v1 │ │ ├── getUserIdentifier.ts │ │ ├── faq │ │ ├── getEResidentFaq.ts │ │ ├── getFaq.ts │ │ ├── createFaq.ts │ │ └── updateFaq.ts │ │ ├── getAppSettings.ts │ │ ├── testAcquirerProviderEncodedData.ts │ │ ├── getAppVersion.ts │ │ ├── getPublicServices.ts │ │ ├── bankIdCallback.ts │ │ ├── getEResidentAppVersion.ts │ │ ├── bankIdAuthCodeResult.ts │ │ ├── errorTemplate │ │ ├── getErrorTemplateByErrorCode.ts │ │ ├── getErrorTemplatesList.ts │ │ ├── createErrorTemplate.ts │ │ └── updateErrorTemplate.ts │ │ ├── unauthorizedSimulate.ts │ │ ├── bumpStoreTags.ts │ │ ├── testAcquirerProviderResponse.ts │ │ └── testAcquirerProviderResponses.ts ├── services │ ├── storeManagement.ts │ ├── partner.ts │ ├── user.ts │ ├── notification.ts │ ├── version.ts │ ├── settings.ts │ └── minioStorage.ts ├── validation │ ├── sandbox.ts │ ├── header.ts │ └── files.ts ├── bootstrap.ts ├── apiDocs │ └── openApiNodeConnectedEvent.ts ├── validationRules │ └── faqCategory.ts ├── externalEventListeners │ └── notification │ │ ├── notificationTemplateCreate.ts │ │ ├── notificationDistributionCancel.ts │ │ ├── notificationDistributionCreate.ts │ │ ├── notificationDistributionStatus.ts │ │ └── notificationDistributionStatusRecipients.ts └── deps.ts ├── .eslintignore ├── tests ├── interfaces │ └── utils.ts ├── mocks │ ├── randomData.ts │ ├── apiDocs │ │ └── route.mock.ts │ ├── index.ts │ └── middlewares │ │ └── fileUpload.ts ├── unit │ ├── index.spec.ts │ ├── utils │ │ └── transformers.spec.ts │ ├── validation │ │ ├── sandbox.spec.ts │ │ └── header.spec.ts │ ├── actions │ │ └── v1 │ │ │ ├── getUserIdentifier.spec.ts │ │ │ ├── bankIdCallback.spec.ts │ │ │ ├── testAcquirerProviderEncodedData.spec.ts │ │ │ ├── bankIdAuthCodeResult.spec.ts │ │ │ ├── getAppVersion.spec.ts │ │ │ ├── getAppSettings.spec.ts │ │ │ ├── getEResidentAppVersion.spec.ts │ │ │ ├── bumpStoreTags.spec.ts │ │ │ ├── faq │ │ │ ├── getEResidentFaq.spec.ts │ │ │ ├── createFaq.spec.ts │ │ │ ├── updateFaq.spec.ts │ │ │ └── getFaq.spec.ts │ │ │ ├── getPublicServices.spec.ts │ │ │ ├── errorTemplate │ │ │ ├── updateErrorTemplate.spec.ts │ │ │ ├── getErrorTemplateByErrorCode.spec.ts │ │ │ ├── createErrorTemplate.spec.ts │ │ │ └── getErrorTemplatesList.spec.ts │ │ │ ├── testAcquirerProviderResponse.spec.ts │ │ │ └── unauthorizedSimulate.spec.ts │ ├── providers │ │ └── faq │ │ │ ├── mongodbFaqProvider.spec.ts │ │ │ └── strapiFaqProvider.spec.ts │ ├── services │ │ ├── partner.spec.ts │ │ ├── user.spec.ts │ │ ├── storeManagement.spec.ts │ │ ├── notification.spec.ts │ │ └── version.spec.ts │ └── dataMappers │ │ └── publicServiceDataMapper.spec.ts ├── tsconfig.json └── utils │ ├── getApp.ts │ └── getDeps.ts ├── migrate-mongo-config.ts ├── scripts ├── generateEnv.sh └── normalizeProcessCodes.js ├── eResidentErrorTemplates.json ├── migrations ├── sample-migration.ts ├── 20240202121416-add-sub-features-to-categories-faq.ts ├── 20240205173015-update-e-resident-faq-order.ts ├── 20220610152143-update-e-resident-faq.ts ├── tsconfig.json ├── 20240313113029-add-e-resident-error-templates.ts ├── 20240202131000-update-e-resident-faq.ts ├── 20230221142216-category-separate-doc.ts ├── 1-migrate-from-js-to-ts.ts ├── 20220523155012-add-error-templates.ts └── 20220505111351-add-faq-for-user.ts ├── .github └── workflows │ └── accept-contribution.yml ├── processCodesTemplates ├── feed.json ├── payment.json ├── invincibility.json ├── address.json └── backOfficePetition.json ├── eResidentProcessCodesTemplates ├── publicService.json └── documentAcquirers.json ├── cabinetProcessCodesTemplates └── auth.json ├── tsconfig.json ├── autoMergeRequest.sh ├── errorTemplates.json └── CONTRIBUTING.md /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | coverage 3 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | MONGO_DATABASE=test-diia-gateway 2 | -------------------------------------------------------------------------------- /src/interfaces/validation/sandbox.ts: -------------------------------------------------------------------------------- 1 | export type ValidationPredicate = () => Error | undefined 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | migrations 5 | migrate-mongo-config.ts 6 | scripts -------------------------------------------------------------------------------- /src/interfaces/types/config.ts: -------------------------------------------------------------------------------- 1 | import config from '@src/config' 2 | 3 | export type AppConfig = Awaited> 4 | -------------------------------------------------------------------------------- /tests/interfaces/utils.ts: -------------------------------------------------------------------------------- 1 | import TestKit from '@diia-inhouse/test' 2 | 3 | export interface TestDeps { 4 | testKit: TestKit 5 | } 6 | -------------------------------------------------------------------------------- /src/interfaces/actions/v1/getFaq.ts: -------------------------------------------------------------------------------- 1 | import { FaqResponse } from '@interfaces/services/faq' 2 | 3 | export type ActionResult = FaqResponse 4 | -------------------------------------------------------------------------------- /tests/mocks/randomData.ts: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'node:crypto' 2 | 3 | export function generateUuid(): string { 4 | return randomUUID() 5 | } 6 | -------------------------------------------------------------------------------- /src/interfaces/services/documents.ts: -------------------------------------------------------------------------------- 1 | export enum DocumentType { 2 | InternalPassport = 'internal-passport', 3 | ForeignPassport = 'foreign-passport', 4 | } 5 | -------------------------------------------------------------------------------- /src/interfaces/routes/mortgage.ts: -------------------------------------------------------------------------------- 1 | export enum PartnerScopeType { 2 | mortgage = 'mortgage', 3 | } 4 | 5 | export enum PartnerMortgageScope { 6 | All = 'all', 7 | } 8 | -------------------------------------------------------------------------------- /src/interfaces/services/version.ts: -------------------------------------------------------------------------------- 1 | export enum MinAppVersionConfigType { 2 | MinAppVersion = 'minAppVersion', 3 | MinEResidentAppVersion = 'minEResidentAppVersion', 4 | } 5 | -------------------------------------------------------------------------------- /migrate-mongo-config.ts: -------------------------------------------------------------------------------- 1 | require('dotenv-flow').config({ silent: true }) 2 | 3 | import { MongoHelper } from '@diia-inhouse/db' 4 | 5 | module.exports = MongoHelper.getMigrateMongoConfig() 6 | -------------------------------------------------------------------------------- /src/interfaces/routes/marriage.ts: -------------------------------------------------------------------------------- 1 | export enum PartnerScopeType { 2 | maintenance = 'maintenance', 3 | } 4 | 5 | export enum PartnerMaintenanceScope { 6 | Admin = 'admin', 7 | } 8 | -------------------------------------------------------------------------------- /src/interfaces/routes/file.ts: -------------------------------------------------------------------------------- 1 | export enum PartnerScopeType { 2 | upload = 'upload', 3 | } 4 | 5 | export enum PartnerUploadScope { 6 | Image = 'image', 7 | Video = 'video', 8 | } 9 | -------------------------------------------------------------------------------- /src/interfaces/services/partner.ts: -------------------------------------------------------------------------------- 1 | export type PartnerScopes = Record 2 | 3 | export interface GetPartnerByTokenResult { 4 | _id: string 5 | scopes: PartnerScopes 6 | } 7 | -------------------------------------------------------------------------------- /scripts/generateEnv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Use "$@" to represent all the arguments 4 | for envVar in "$@" 5 | do 6 | # Hotfix: remove escaped characters by \\ 7 | echo "${envVar//\\\\/}" >> .env 8 | done -------------------------------------------------------------------------------- /src/providers/faq/index.ts: -------------------------------------------------------------------------------- 1 | export { default as MongodbFaqProvider } from '@src/providers/faq/mongodbFaqProvider' 2 | 3 | export { default as StrapiFaqProvider } from '@src/providers/faq/strapiFaqProvider' 4 | -------------------------------------------------------------------------------- /src/interfaces/services/user.ts: -------------------------------------------------------------------------------- 1 | import { DiiaOfficeProfileData, ProfileFeature } from '@diia-inhouse/types' 2 | 3 | export interface UserProfileFeatures { 4 | [ProfileFeature.office]?: DiiaOfficeProfileData 5 | } 6 | -------------------------------------------------------------------------------- /src/interfaces/actions/v1/getPublicServices.ts: -------------------------------------------------------------------------------- 1 | import { PublicServiceResponse } from '@interfaces/services/publicServicesList' 2 | 3 | export interface ActionResult { 4 | publicServices: PublicServiceResponse[] 5 | } 6 | -------------------------------------------------------------------------------- /src/interfaces/routes/index.ts: -------------------------------------------------------------------------------- 1 | export interface MoleculerAliases { 2 | [name: string]: unknown[] 3 | } 4 | 5 | export interface BuildRoutesResult { 6 | aliases: MoleculerAliases 7 | preserveRawBodyIn: string[] 8 | } 9 | -------------------------------------------------------------------------------- /tests/mocks/apiDocs/route.mock.ts: -------------------------------------------------------------------------------- 1 | export const expectedRoute = { 2 | path: '/swagger-path', 3 | mergeParams: true, 4 | bodyParsers: { json: true, urlencoded: { extended: true } }, 5 | authorization: true, 6 | } 7 | -------------------------------------------------------------------------------- /src/interfaces/actions/v1/getUserIdentifier.ts: -------------------------------------------------------------------------------- 1 | import { UserActionArguments } from '@diia-inhouse/types' 2 | 3 | export type CustomActionArguments = UserActionArguments 4 | 5 | export interface ActionResult { 6 | identifier: string 7 | } 8 | -------------------------------------------------------------------------------- /src/interfaces/actions/v1/unauthorizedSimulate.ts: -------------------------------------------------------------------------------- 1 | import { UserActionArguments } from '@diia-inhouse/types' 2 | 3 | export type CustomActionArguments = UserActionArguments 4 | 5 | export interface ActionResult { 6 | status: string 7 | } 8 | -------------------------------------------------------------------------------- /src/interfaces/routes/payment.ts: -------------------------------------------------------------------------------- 1 | export enum PartnerScopeType { 2 | maintenance = 'maintenance', 3 | payment = 'payment', 4 | } 5 | 6 | export enum PartnerPaymentScope { 7 | Debt = 'debt', 8 | Penalty = 'penalty', 9 | } 10 | -------------------------------------------------------------------------------- /src/interfaces/actions/v1/testAcquirerProviderEncodedData.ts: -------------------------------------------------------------------------------- 1 | import { ServiceActionArguments } from '@diia-inhouse/types' 2 | 3 | export type CustomActionArguments = ServiceActionArguments 4 | 5 | export interface ActionResult { 6 | success: boolean 7 | } 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { initTracing } from '@diia-inhouse/diia-app' 2 | 3 | const serviceName = 'Gateway' 4 | 5 | initTracing(serviceName) 6 | 7 | import 'module-alias/register' 8 | import { bootstrap } from './bootstrap' 9 | 10 | bootstrap(serviceName) 11 | -------------------------------------------------------------------------------- /src/interfaces/externalEventListeners/notification/index.ts: -------------------------------------------------------------------------------- 1 | export interface EventPayloadRequest extends Record { 2 | partnerId: string 3 | } 4 | 5 | export interface EventPayload { 6 | uuid: string 7 | request: EventPayloadRequest 8 | } 9 | -------------------------------------------------------------------------------- /src/interfaces/routes/documentDelivery.ts: -------------------------------------------------------------------------------- 1 | export enum PartnerScopeType { 2 | documentDelivery = 'documentDelivery', 3 | } 4 | 5 | export enum PartnerDocumentDeliveryScope { 6 | StatusCallback = 'statusCallback', 7 | CreateDelivery = 'createDelivery', 8 | } 9 | -------------------------------------------------------------------------------- /src/interfaces/routes/publicServiceCatalog.ts: -------------------------------------------------------------------------------- 1 | export enum PartnerScopeType { 2 | publicService = 'publicService', 3 | } 4 | 5 | export enum PartnerPublicServiceScope { 6 | Read = 'read', 7 | Create = 'create', 8 | Update = 'update', 9 | } 10 | -------------------------------------------------------------------------------- /src/interfaces/services/settings.ts: -------------------------------------------------------------------------------- 1 | export enum AppAction { 2 | PushTokenUpdate = 'pushTokenUpdate', 3 | ForceUpdate = 'forceUpdate', 4 | } 5 | 6 | export interface AppSettings { 7 | minVersion: string | null 8 | needActions: AppAction[] 9 | } 10 | -------------------------------------------------------------------------------- /src/interfaces/actions/v1/bankIdCallback.ts: -------------------------------------------------------------------------------- 1 | import { ServiceActionArguments } from '@diia-inhouse/types' 2 | 3 | export interface CustomActionArguments extends ServiceActionArguments { 4 | code: string 5 | state?: string 6 | error?: string 7 | error_description?: string 8 | } 9 | -------------------------------------------------------------------------------- /src/interfaces/routes/depositGuaranteePayments.ts: -------------------------------------------------------------------------------- 1 | export enum PartnerScopeType { 2 | maintenance = 'maintenance', 3 | depositGuaranteePayments = 'depositGuaranteePayments', 4 | } 5 | 6 | export enum PartnerDepositGuaranteePaymentsScope { 7 | UpdateStatus = 'update-status', 8 | } 9 | -------------------------------------------------------------------------------- /src/interfaces/actions/v1/bankIdAuthCodeResult.ts: -------------------------------------------------------------------------------- 1 | import { ServiceActionArguments } from '@diia-inhouse/types' 2 | 3 | export interface CustomActionArguments extends ServiceActionArguments { 4 | code: string 5 | state?: string 6 | error?: string 7 | error_description?: string 8 | } 9 | -------------------------------------------------------------------------------- /src/interfaces/actions/v1/getAppVersion.ts: -------------------------------------------------------------------------------- 1 | import { AppUserActionHeaders, ServiceActionArguments } from '@diia-inhouse/types' 2 | 3 | export type CustomActionArguments = ServiceActionArguments 4 | 5 | export interface ActionResult { 6 | minVersion: string | null 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/transformers.ts: -------------------------------------------------------------------------------- 1 | import { Response } from '@src/interfaces' 2 | 3 | export function jsonResponseTransformer(res: Response, externalResponse: unknown): string { 4 | res.setHeader('Content-Type', 'application/json; charset=utf-8') 5 | 6 | return JSON.stringify(externalResponse) 7 | } 8 | -------------------------------------------------------------------------------- /src/interfaces/models/errorTemplate.ts: -------------------------------------------------------------------------------- 1 | import { Document } from '@diia-inhouse/db' 2 | 3 | export interface ErrorTemplate { 4 | errorCode: number 5 | template: { 6 | description: string 7 | } 8 | } 9 | 10 | export interface ErrorTemplateModel extends ErrorTemplate, Document {} 11 | -------------------------------------------------------------------------------- /src/interfaces/providers/index.ts: -------------------------------------------------------------------------------- 1 | import { SessionType, UserFeatures } from '@diia-inhouse/types' 2 | 3 | import { FaqCategory } from '@interfaces/models/faqCategory' 4 | 5 | export interface FaqProvider { 6 | getCategoriesList(session: SessionType, userFeatures: UserFeatures): Promise 7 | } 8 | -------------------------------------------------------------------------------- /src/interfaces/actions/v1/faq/createFaq.ts: -------------------------------------------------------------------------------- 1 | import { PartnerActionArguments } from '@diia-inhouse/types' 2 | 3 | import { Faq } from '@interfaces/services/faq' 4 | 5 | export interface CustomActionArguments extends PartnerActionArguments { 6 | params: Faq 7 | } 8 | 9 | export type ActionResult = Faq 10 | -------------------------------------------------------------------------------- /src/interfaces/actions/v1/faq/updateFaq.ts: -------------------------------------------------------------------------------- 1 | import { PartnerActionArguments } from '@diia-inhouse/types' 2 | 3 | import { Faq } from '@interfaces/services/faq' 4 | 5 | export interface CustomActionArguments extends PartnerActionArguments { 6 | params: Faq 7 | } 8 | 9 | export type ActionResult = Faq 10 | -------------------------------------------------------------------------------- /src/interfaces/actions/v1/getAppSettings.ts: -------------------------------------------------------------------------------- 1 | import { AppUserActionHeaders, ServiceActionArguments } from '@diia-inhouse/types' 2 | 3 | import { AppSettings } from '@interfaces/services/settings' 4 | 5 | export type CustomActionArguments = ServiceActionArguments 6 | 7 | export type ActionResult = AppSettings 8 | -------------------------------------------------------------------------------- /eResidentErrorTemplates.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "errorCode": 11012, 4 | "template": { 5 | "description": "A non-existent message template identifier was specified" 6 | } 7 | }, 8 | { 9 | "errorCode": 11013, 10 | "template": { 11 | "description": "Validation error" 12 | } 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /src/dataMappers/publicServiceDataMapper.ts: -------------------------------------------------------------------------------- 1 | import { PublicService, PublicServiceResponse } from '@interfaces/services/publicServicesList' 2 | 3 | export default class BankDataMapper { 4 | toEntity(publicService: PublicService): PublicServiceResponse { 5 | const { sortOrder, ...response } = publicService 6 | 7 | return response 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/interfaces/actions/v1/errorTemplates/createErrorTemplate.ts: -------------------------------------------------------------------------------- 1 | import { PartnerActionArguments } from '@diia-inhouse/types' 2 | 3 | import { ErrorTemplate } from '@interfaces/models/errorTemplate' 4 | 5 | export interface CustomActionArguments extends PartnerActionArguments { 6 | params: ErrorTemplate 7 | } 8 | 9 | export type ActionResult = ErrorTemplate 10 | -------------------------------------------------------------------------------- /src/interfaces/actions/v1/errorTemplates/updateErrorTemplate.ts: -------------------------------------------------------------------------------- 1 | import { PartnerActionArguments } from '@diia-inhouse/types' 2 | 3 | import { ErrorTemplate } from '@interfaces/models/errorTemplate' 4 | 5 | export interface CustomActionArguments extends PartnerActionArguments { 6 | params: ErrorTemplate 7 | } 8 | 9 | export type ActionResult = ErrorTemplate 10 | -------------------------------------------------------------------------------- /src/interfaces/externalEventListeners/index.ts: -------------------------------------------------------------------------------- 1 | export interface MessageError { 2 | http_code: number 3 | message: string 4 | errorCode?: number 5 | description?: string 6 | data?: Record 7 | } 8 | 9 | export interface MessagePayload { 10 | uuid: string 11 | error?: MessageError 12 | response?: T 13 | } 14 | -------------------------------------------------------------------------------- /src/interfaces/actions/v1/bumpStoreTags.ts: -------------------------------------------------------------------------------- 1 | import { StoreTag } from '@diia-inhouse/redis' 2 | import { PartnerActionArguments } from '@diia-inhouse/types' 3 | 4 | export interface CustomActionArguments extends PartnerActionArguments { 5 | params: { 6 | tags: StoreTag[] 7 | } 8 | } 9 | 10 | export interface ActionResult { 11 | success: true 12 | } 13 | -------------------------------------------------------------------------------- /src/interfaces/actions/v1/testAcquirerProviderResponse.ts: -------------------------------------------------------------------------------- 1 | import { ServiceActionArguments } from '@diia-inhouse/types' 2 | 3 | export interface CustomActionArguments extends ServiceActionArguments { 4 | encryptedFile: Buffer 5 | encryptedFileName: string 6 | encodeData: string 7 | } 8 | 9 | export interface ActionResult { 10 | success: boolean 11 | error?: string 12 | } 13 | -------------------------------------------------------------------------------- /src/interfaces/routes/documentAcquirers.ts: -------------------------------------------------------------------------------- 1 | export enum PartnerScopeType { 2 | acquirers = 'acquirers', 3 | administrativeFees = 'administrativeFees', 4 | } 5 | 6 | export enum PartnerAcquirersScope { 7 | Create = 'create', 8 | Read = 'read', 9 | Update = 'update', 10 | Delete = 'delete', 11 | } 12 | 13 | export enum PartnerAdministrativeFeesScope { 14 | Load = 'load', 15 | } 16 | -------------------------------------------------------------------------------- /src/interfaces/routes/vehicleReRegistration.ts: -------------------------------------------------------------------------------- 1 | export enum PartnerScopeType { 2 | mia = 'mia', 3 | vehicleReRegistration = 'vehicleReRegistration', 4 | } 5 | 6 | export enum PartnerMiaScope { 7 | OperationCallback = 'operation-callback', 8 | ResultPushCallback = 'result-push-callback', 9 | } 10 | 11 | export enum PartnerVehicleReRegistrationScope { 12 | Support = 'support', 13 | } 14 | -------------------------------------------------------------------------------- /src/interfaces/actions/v1/errorTemplates/getErrorTemplateByErrorCode.ts: -------------------------------------------------------------------------------- 1 | import { PartnerActionArguments } from '@diia-inhouse/types' 2 | 3 | import { ErrorTemplateResult } from '@interfaces/services/errorTemplate' 4 | 5 | export interface CustomActionArguments extends PartnerActionArguments { 6 | params: { 7 | errorCode: number 8 | } 9 | } 10 | 11 | export type ActionResult = ErrorTemplateResult 12 | -------------------------------------------------------------------------------- /tests/mocks/index.ts: -------------------------------------------------------------------------------- 1 | import { AsyncLocalStorage } from 'node:async_hooks' 2 | 3 | import DiiaLogger from '@diia-inhouse/diia-logger' 4 | import { QueueContext } from '@diia-inhouse/diia-queue' 5 | import { mockClass } from '@diia-inhouse/test' 6 | 7 | export const logger = new (mockClass(DiiaLogger))() 8 | 9 | export const asyncLocalStorageMock = >({ 10 | getStore: jest.fn(), 11 | }) 12 | -------------------------------------------------------------------------------- /src/interfaces/actions/v1/errorTemplates/getErrorTemplatesList.ts: -------------------------------------------------------------------------------- 1 | import { PartnerActionArguments } from '@diia-inhouse/types' 2 | 3 | import { ErrorTemplateListResult } from '@interfaces/services/errorTemplate' 4 | 5 | export interface CustomActionArguments extends PartnerActionArguments { 6 | params: { 7 | skip?: number 8 | limit?: number 9 | } 10 | } 11 | 12 | export type ActionResult = ErrorTemplateListResult 13 | -------------------------------------------------------------------------------- /migrations/sample-migration.ts: -------------------------------------------------------------------------------- 1 | import 'module-alias/register' 2 | import { config } from 'dotenv-flow' 3 | import { Db } from 'mongodb' 4 | 5 | config({ silent: true }) 6 | 7 | const collectionName = '' 8 | 9 | export async function up(db: Db): Promise { 10 | // await db.createCollection(collectionName) 11 | } 12 | 13 | export async function down(db: Db): Promise { 14 | // await db.dropCollection(collectionName) 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/accept-contribution.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | inputs: 4 | pr_number: 5 | description: Number of accepted PR 6 | required: true 7 | 8 | jobs: 9 | accept-contribution: 10 | with: 11 | pr_number: ${{ inputs.pr_number }} 12 | uses: diia-open-source/reusable-workflows/.github/workflows/accept-contribution-be.yml@main 13 | secrets: inherit 14 | -------------------------------------------------------------------------------- /migrations/20240202121416-add-sub-features-to-categories-faq.ts: -------------------------------------------------------------------------------- 1 | import 'module-alias/register' 2 | 3 | import { Db } from 'mongodb' 4 | 5 | const categoryCollectionName = 'faqcategories' 6 | 7 | export async function up(db: Db): Promise { 8 | db.collection(categoryCollectionName).updateMany( 9 | {}, 10 | { $set: { 'faq.$[el].subFeatures': [] } }, 11 | { arrayFilters: [{ 'el.subFeatures': { $exists: false } }] }, 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/interfaces/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from '@interfaces/index' 2 | 3 | export type MiddlewareNext = (err?: Error) => void 4 | 5 | export type Middleware = (req: Request, res: Response, next: MiddlewareNext) => Promise | void 6 | 7 | export type FileUploadHandler = (req: Request, res: Response, next: (err?: Error | unknown) => void) => Promise 8 | 9 | export type FilesUploadHandler = (req: Request, res: Response, next: (err?: Error | unknown) => void) => void 10 | -------------------------------------------------------------------------------- /src/interfaces/services/publicServicesList.ts: -------------------------------------------------------------------------------- 1 | export enum PublicServiceStatus { 2 | InDevelopment = 'inDevelopment', 3 | Inactive = 'inactive', 4 | Archival = 'archival', 5 | Active = 'active', 6 | } 7 | 8 | interface BasePublicService { 9 | code: string 10 | name: string 11 | status: PublicServiceStatus 12 | } 13 | 14 | export interface PublicService extends BasePublicService { 15 | sortOrder: number 16 | } 17 | 18 | export type PublicServiceResponse = BasePublicService 19 | -------------------------------------------------------------------------------- /src/interfaces/services/faq.ts: -------------------------------------------------------------------------------- 1 | import { SessionType } from '@diia-inhouse/types' 2 | 3 | import { FaqCategory } from '@interfaces/models/faqCategory' 4 | 5 | type FaqCategoryReq = Omit 6 | type FaqCategoryRes = Omit 7 | 8 | export interface Faq { 9 | session: SessionType 10 | categories: FaqCategoryReq[] 11 | } 12 | 13 | export interface FaqResponse { 14 | categories: FaqCategoryRes[] 15 | expirationDate: string 16 | } 17 | -------------------------------------------------------------------------------- /src/routes/verifier.ts: -------------------------------------------------------------------------------- 1 | import { ActionVersion, HttpMethod, SessionType } from '@diia-inhouse/types' 2 | 3 | import { AppRoute } from '@interfaces/routes/appRoute' 4 | 5 | const serviceId = 'verifier' 6 | 7 | const routes: AppRoute[] = [ 8 | { 9 | method: HttpMethod.GET, 10 | path: '/api/:apiVersion/net/certificates', 11 | proxyTo: { serviceId }, 12 | auth: [{ sessionType: SessionType.None, version: ActionVersion.V1 }], 13 | headers: [], 14 | }, 15 | ] 16 | 17 | export default routes 18 | -------------------------------------------------------------------------------- /src/interfaces/routes/gateway.ts: -------------------------------------------------------------------------------- 1 | export enum PartnerScopeType { 2 | faq = 'faq', 3 | processTemplate = 'processTemplate', 4 | errorTemplate = 'errorTemplate', 5 | store = 'store', 6 | } 7 | 8 | export enum PartnerFaqScope { 9 | Read = 'read', 10 | Create = 'create', 11 | Update = 'update', 12 | } 13 | 14 | export enum PartnerErrorTemplateScope { 15 | Read = 'read', 16 | Create = 'create', 17 | Update = 'update', 18 | } 19 | 20 | export enum PartnerStoreScope { 21 | BumpTags = 'bump-tags', 22 | } 23 | -------------------------------------------------------------------------------- /src/interfaces/services/errorTemplate.ts: -------------------------------------------------------------------------------- 1 | import { ErrorTemplate } from '@interfaces/models/errorTemplate' 2 | 3 | export interface ErrorTemplateFileItem { 4 | errorCode: number 5 | template: ErrorTemplate 6 | } 7 | 8 | export interface ErrorTemplateResult extends ErrorTemplate { 9 | id: string 10 | } 11 | 12 | export interface ErrorTemplateListResult { 13 | errorTemplates: ErrorTemplateResult[] 14 | total: number 15 | } 16 | 17 | export interface GetErrorTemplatesListOptions { 18 | skip: number 19 | limit: number 20 | } 21 | -------------------------------------------------------------------------------- /src/interfaces/routes/partner.ts: -------------------------------------------------------------------------------- 1 | export enum PartnerScopeType { 2 | vaccinationAid = 'vaccinationAid', 3 | partners = 'partners', 4 | maintenance = 'maintenance', 5 | } 6 | 7 | export enum PartnerVaccinationAidScope { 8 | AddAccount = 'addAccount', 9 | DeleteAccount = 'deleteAccount', 10 | ProcessReport = 'processReport', 11 | ProcessPfuReport = 'processPfuReport', 12 | } 13 | 14 | export enum PartnerPartnersScope { 15 | Create = 'create', 16 | } 17 | 18 | export enum PartnerMaintenanceScope { 19 | Admin = 'admin', 20 | } 21 | -------------------------------------------------------------------------------- /src/dataMappers/errorTemplateDataMapper.ts: -------------------------------------------------------------------------------- 1 | import { ErrorTemplateModel } from '@interfaces/models/errorTemplate' 2 | import { ErrorTemplateResult } from '@interfaces/services/errorTemplate' 3 | 4 | export default class ErrorTemplateDataMapper { 5 | toEntity(model: ErrorTemplateModel): ErrorTemplateResult { 6 | const { _id: id, errorCode, template } = model 7 | const result: ErrorTemplateResult = { 8 | id: id.toString(), 9 | errorCode, 10 | template, 11 | } 12 | 13 | return result 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /processCodesTemplates/feed.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "processCode": 55101001, 4 | "template": { 5 | "type": "middleCenterAlignAlert", 6 | "isClosable": false, 7 | "data": { 8 | "icon": "😔", 9 | "title": "Новини не існує", 10 | "description": "На жаль, новину видалили або посилання на неї більше недійсне.", 11 | "mainButton": { 12 | "name": "Зрозуміло", 13 | "action": "back" 14 | } 15 | } 16 | } 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /src/models/errorTemplate.ts: -------------------------------------------------------------------------------- 1 | import { Model, Schema, model, models } from '@diia-inhouse/db' 2 | 3 | import { ErrorTemplate } from '@interfaces/models/errorTemplate' 4 | 5 | const errorTemplateSchema = new Schema( 6 | { 7 | errorCode: { type: Number, unique: true, required: true }, 8 | template: { 9 | description: { type: String, required: true }, 10 | }, 11 | }, 12 | { 13 | timestamps: true, 14 | }, 15 | ) 16 | 17 | export default >models.ErrorTemplate || model('ErrorTemplate', errorTemplateSchema) 18 | -------------------------------------------------------------------------------- /eResidentProcessCodesTemplates/publicService.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "processCode": 117101004, 4 | "template": { 5 | "type": "middleCenterAlignAlert", 6 | "isClosable": false, 7 | "data": { 8 | "icon": "😞", 9 | "title": "Service is unavailable", 10 | "description": "Service is temporarily unavailable. Please try again later", 11 | "mainButton": { 12 | "name": "OK", 13 | "action": "skip" 14 | } 15 | } 16 | } 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /tests/unit/index.spec.ts: -------------------------------------------------------------------------------- 1 | const initTracing = jest.fn() 2 | const bootstrap = jest.fn() 3 | 4 | jest.mock('@diia-inhouse/diia-app', () => ({ 5 | ...jest.requireActual('@diia-inhouse/diia-app'), 6 | initTracing, 7 | })) 8 | 9 | jest.mock('@src/bootstrap', () => ({ 10 | bootstrap, 11 | })) 12 | 13 | describe('Index', () => { 14 | it('should initTracing and call app bootstrap', () => { 15 | // eslint-disable-next-line n/no-missing-require 16 | require('@src/index') 17 | expect(initTracing).toHaveBeenCalledWith('Gateway') 18 | expect(bootstrap).toHaveBeenCalledWith('Gateway') 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /src/middlewares/redirect.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@diia-inhouse/types' 2 | 3 | import { Request, Response } from '@interfaces/index' 4 | 5 | export default class RedirectMiddleware { 6 | constructor(private readonly logger: Logger) {} 7 | 8 | addRedirect(url: string): (req: Request, res: Response, next: (err?: Error) => void) => void { 9 | return (req: Request, _res: Response, next: (err?: Error) => void): void => { 10 | this.logger.debug(`Redirect to [${url}] with data [${JSON.stringify(req.$params.params)}]`) 11 | 12 | req.$params.redirect = url 13 | 14 | next() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /eResidentProcessCodesTemplates/documentAcquirers.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "processCode": 112141001, 4 | "template": { 5 | "type": "middleCenterAlignAlert", 6 | "isClosable": false, 7 | "data": { 8 | "icon": "😞", 9 | "title": "Sorry, the request has not been fulfilled.", 10 | "description": "Data on some of requested documents is temporarily unavailable. Please try again later.", 11 | "mainButton": { 12 | "name": "Ok", 13 | "action": "skip" 14 | } 15 | } 16 | } 17 | } 18 | ] 19 | -------------------------------------------------------------------------------- /src/interfaces/models/cms/faq.ts: -------------------------------------------------------------------------------- 1 | import { CmsBaseAttributes, CmsEntries } from '@diia-inhouse/cms' 2 | import { ProfileFeature, SessionType } from '@diia-inhouse/types' 3 | 4 | import { FaqParameter } from '@interfaces/models/faqCategory' 5 | 6 | export interface CmsFaq extends CmsBaseAttributes { 7 | question: string 8 | answer: string 9 | parameters: FaqParameter[] 10 | } 11 | 12 | export interface CmsFaqCategory extends CmsBaseAttributes { 13 | code: string 14 | name: string 15 | sessionType: SessionType 16 | faq: CmsEntries 17 | features?: { 18 | value: ProfileFeature 19 | }[] 20 | order: number 21 | } 22 | -------------------------------------------------------------------------------- /migrations/20240205173015-update-e-resident-faq-order.ts: -------------------------------------------------------------------------------- 1 | import 'module-alias/register' 2 | import { Db } from 'mongodb' 3 | 4 | import { SessionType } from '@diia-inhouse/types' 5 | 6 | const collectionName = 'faqcategories' 7 | 8 | export async function up(db: Db): Promise { 9 | const operations = ['general', 'yourBusiness', 'otherQuestions'].map((code, index) => ({ 10 | updateOne: { 11 | filter: { code, sessionType: SessionType.EResident }, 12 | update: { 13 | $set: { order: 1000 + index + 1 }, 14 | }, 15 | }, 16 | })) 17 | 18 | await db.collection(collectionName).bulkWrite(operations) 19 | } 20 | -------------------------------------------------------------------------------- /src/interfaces/config.ts: -------------------------------------------------------------------------------- 1 | import { AuthConfig as CoreAuthConfig } from '@diia-inhouse/crypto' 2 | 3 | interface MinioConfigDisabled { 4 | isEnabled: false 5 | host: undefined 6 | port: undefined 7 | accessKey: undefined 8 | secretKey: undefined 9 | } 10 | 11 | interface MinioConfigEnabled { 12 | isEnabled: true 13 | host: string 14 | port: number 15 | accessKey: string 16 | secretKey: string 17 | } 18 | 19 | export type MinioConfig = MinioConfigDisabled | MinioConfigEnabled 20 | 21 | export interface AuthConfig extends CoreAuthConfig { 22 | authHeader: string 23 | authSchema: string 24 | deviceHeaderUuidVersions: string[] 25 | } 26 | -------------------------------------------------------------------------------- /src/interfaces/routes/notification.ts: -------------------------------------------------------------------------------- 1 | export enum PartnerScopeType { 2 | notifications = 'notifications', 3 | maintenance = 'maintenance', 4 | } 5 | 6 | export enum PartnerNotificationsScope { 7 | CreateTemplate = 'createTemplate', 8 | CreateDistribution = 'createDistribution', 9 | DistributionStatus = 'distributionStatus', 10 | DistributionResults = 'distributionResults', 11 | DeleteDistribution = 'deleteDistribution', 12 | StartNotificationsByAppVersions = 'startNotificationsByAppVersions', 13 | PushTopic = 'pushTopic', 14 | PushCampaign = 'pushCampaign', 15 | } 16 | 17 | export enum PartnerMaintenanceScope { 18 | Admin = 'admin', 19 | } 20 | -------------------------------------------------------------------------------- /src/actions/v1/getUserIdentifier.ts: -------------------------------------------------------------------------------- 1 | import { AppAction } from '@diia-inhouse/diia-app' 2 | 3 | import { ActionVersion, SessionType } from '@diia-inhouse/types' 4 | 5 | import { ActionResult, CustomActionArguments } from '@interfaces/actions/v1/getUserIdentifier' 6 | 7 | export default class GetUserIdentifierAction implements AppAction { 8 | readonly sessionType: SessionType = SessionType.User 9 | 10 | readonly actionVersion: ActionVersion = ActionVersion.V1 11 | 12 | readonly name: string = 'getUserIdentifier' 13 | 14 | async handler(args: CustomActionArguments): Promise { 15 | return { identifier: args.session.user.identifier } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/interfaces/services/processData.ts: -------------------------------------------------------------------------------- 1 | export interface ProcessData { 2 | processCode: number 3 | template: { 4 | type: string 5 | isClosable: false 6 | data: TemplateData 7 | } 8 | alert?: Record 9 | } 10 | 11 | export interface TemplateButton { 12 | name: string 13 | action: string 14 | resource?: string 15 | } 16 | 17 | export interface TemplateData { 18 | icon: string 19 | title: string 20 | description: string 21 | mainButton?: TemplateButton 22 | alternativeButton?: TemplateButton 23 | } 24 | 25 | export interface ProcessDataParams { 26 | templateParams?: Record 27 | resource?: string 28 | } 29 | -------------------------------------------------------------------------------- /src/services/storeManagement.ts: -------------------------------------------------------------------------------- 1 | import { InternalServerError } from '@diia-inhouse/errors' 2 | import { StoreService, StoreTag } from '@diia-inhouse/redis' 3 | import { Logger } from '@diia-inhouse/types' 4 | 5 | export default class StoreManagementService { 6 | constructor( 7 | private readonly store: StoreService, 8 | private readonly logger: Logger, 9 | ) {} 10 | 11 | async bumpTags(tags: StoreTag[]): Promise { 12 | const result = await this.store.bumpTags(tags) 13 | 14 | if (!result) { 15 | this.logger.error('Failed to bump tags', { tags }) 16 | 17 | throw new InternalServerError('Failed to bump tags') 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /migrations/20220610152143-update-e-resident-faq.ts: -------------------------------------------------------------------------------- 1 | import 'module-alias/register' 2 | import { readFileSync } from 'fs' 3 | import { resolve } from 'path' 4 | 5 | import { Db } from 'mongodb' 6 | 7 | import { SessionType } from '@diia-inhouse/types' 8 | 9 | const collectionName = 'faqs' 10 | const filepath: string = resolve('eResidentFaq.json') 11 | 12 | export async function up(db: Db): Promise { 13 | const categoriesStr = await readFileSync(filepath).toString() 14 | try { 15 | const categories = JSON.parse(categoriesStr).categories 16 | 17 | await db.collection(collectionName).updateOne({ session: SessionType.EResident }, { $set: { categories } }) 18 | } catch (err) {} 19 | } 20 | -------------------------------------------------------------------------------- /src/services/partner.ts: -------------------------------------------------------------------------------- 1 | import { MoleculerService } from '@diia-inhouse/diia-app' 2 | 3 | import { ActionVersion } from '@diia-inhouse/types' 4 | 5 | import { GetPartnerByTokenResult } from '@interfaces/services/partner' 6 | 7 | export default class PartnerService { 8 | private readonly serviceName: string = 'Partner' 9 | 10 | constructor(private readonly lazyMoleculer: () => MoleculerService) {} 11 | 12 | async getPartnerByToken(partnerToken: string): Promise { 13 | return await this.lazyMoleculer().act( 14 | this.serviceName, 15 | { name: 'getPartnerByToken', actionVersion: ActionVersion.V1 }, 16 | { params: { partnerToken } }, 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/actions/v1/faq/getEResidentFaq.ts: -------------------------------------------------------------------------------- 1 | import { AppAction } from '@diia-inhouse/diia-app' 2 | 3 | import { ActionVersion, SessionType } from '@diia-inhouse/types' 4 | 5 | import FaqService from '@services/faq' 6 | 7 | import { ActionResult } from '@interfaces/actions/v1/getFaq' 8 | 9 | export default class GetEResidentFaqAction implements AppAction { 10 | constructor(private readonly faqService: FaqService) {} 11 | 12 | readonly sessionType: SessionType = SessionType.EResident 13 | 14 | readonly actionVersion: ActionVersion = ActionVersion.V1 15 | 16 | readonly name: string = 'getEResidentFaq' 17 | 18 | async handler(): Promise { 19 | return await this.faqService.getFaq(this.sessionType, {}) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /migrations/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "ts-node": { 4 | "files": true 5 | }, 6 | "compilerOptions": { 7 | "baseUrl": "..", 8 | "paths": { 9 | "@services/*": ["dist/types/services/*"], 10 | "@interfaces/*": ["dist/types/interfaces/*"], 11 | "@models/*": ["dist/types/models/*"], 12 | "@dataMappers/*": ["dist/types/dataMappers/*"], 13 | "@utils/*": ["dist/types/utils/*"], 14 | "@xmlMappings/*": ["dist/types/xmlMappings/*"], 15 | "@actions/*": ["dist/types/actions/*"], 16 | "@pages/*": ["dist/types/pages/*"], 17 | "@src/*": ["dist/types/*"] 18 | } 19 | }, 20 | "include": ["./**/*"] 21 | } 22 | -------------------------------------------------------------------------------- /src/services/user.ts: -------------------------------------------------------------------------------- 1 | import { MoleculerService } from '@diia-inhouse/diia-app' 2 | 3 | import { ActionVersion, ProfileFeature } from '@diia-inhouse/types' 4 | 5 | import { UserProfileFeatures } from '@interfaces/services/user' 6 | 7 | export default class UserService { 8 | private readonly serviceName: string = 'User' 9 | 10 | constructor(private readonly lazyMoleculer: () => MoleculerService) {} 11 | 12 | async getUserProfileFeatures(userIdentifier: string, features: ProfileFeature[]): Promise { 13 | return await this.lazyMoleculer().act( 14 | this.serviceName, 15 | { name: 'getUserProfileFeatures', actionVersion: ActionVersion.V1 }, 16 | { params: { userIdentifier, features } }, 17 | ) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/routes/defaults.ts: -------------------------------------------------------------------------------- 1 | import { ActionVersion, SessionType } from '@diia-inhouse/types' 2 | 3 | import { RouteHeaderRawName } from '@interfaces/index' 4 | import { CustomHeader, RouteAuthParams } from '@interfaces/routes/appRoute' 5 | 6 | export const DEFAULT_USER_HEADERS: CustomHeader[] = [ 7 | { name: RouteHeaderRawName.MOBILE_UID, versions: [ActionVersion.V1] }, 8 | { name: RouteHeaderRawName.APP_VERSION, versions: [ActionVersion.V1] }, 9 | { name: RouteHeaderRawName.PLATFORM_TYPE, versions: [ActionVersion.V1] }, 10 | { name: RouteHeaderRawName.PLATFORM_VERSION, versions: [ActionVersion.V1] }, 11 | ] 12 | 13 | export const DEFAULT_USER_AUTH: RouteAuthParams[] = [ 14 | { 15 | sessionType: SessionType.User, 16 | version: ActionVersion.V1, 17 | }, 18 | ] 19 | -------------------------------------------------------------------------------- /tests/unit/utils/transformers.spec.ts: -------------------------------------------------------------------------------- 1 | import { jsonResponseTransformer } from '@src/utils/transformers' 2 | 3 | import { Response } from '@interfaces/index' 4 | 5 | describe('Transformers', () => { 6 | describe(`method: ${jsonResponseTransformer.name}`, () => { 7 | it('should encode external response into JSON string and set content type header', () => { 8 | const externalResponse = { message: {} } 9 | const res = ({ 10 | setHeader: jest.fn(), 11 | }) 12 | 13 | expect(jsonResponseTransformer(res, externalResponse)).toEqual(JSON.stringify(externalResponse)) 14 | expect(res.setHeader).toHaveBeenCalledWith('Content-Type', 'application/json; charset=utf-8') 15 | }) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "../", 5 | "paths": { 6 | "@services/*": ["src/services/*"], 7 | "@interfaces/*": ["src/interfaces/*"], 8 | "@models/*": ["src/models/*"], 9 | "@dataMappers/*": ["src/dataMappers/*"], 10 | "@utils/*": ["src/utils/*"], 11 | "@xmlMappings/*": ["src/xmlMappings/*"], 12 | "@actions/*": ["src/actions/*"], 13 | "@src/*": ["src/*"], 14 | "@mocks/*": ["tests/mocks/*"], 15 | "@validation/*": ["src/validation/*"], 16 | "@tests/*": ["tests/*"] 17 | }, 18 | "noEmit": true, 19 | "strict": true 20 | }, 21 | "include": ["./**/*"] 22 | } 23 | -------------------------------------------------------------------------------- /tests/unit/validation/sandbox.spec.ts: -------------------------------------------------------------------------------- 1 | import { ValidationSandBox } from '@src/validation/sandbox' 2 | 3 | describe(`${ValidationSandBox.name}`, () => { 4 | describe('method: validate', () => { 5 | it('should successfully run validation process and return error on second step', () => { 6 | const expectedResult = new Error('Invalid input') 7 | const predicateWithoutError = (): Error | undefined => { 8 | return 9 | } 10 | 11 | const predicateWithError = (): Error | undefined => expectedResult 12 | 13 | expect(ValidationSandBox.build(predicateWithoutError).next(ValidationSandBox.build(predicateWithError)).validate()).toEqual( 14 | expectedResult, 15 | ) 16 | }) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /tests/utils/getApp.ts: -------------------------------------------------------------------------------- 1 | import { Application, ServiceContext, ServiceOperator } from '@diia-inhouse/diia-app' 2 | 3 | import config from '@src/config' 4 | 5 | import { TestDeps } from '@tests/interfaces/utils' 6 | import deps from '@tests/utils/getDeps' 7 | 8 | import { AppDeps } from '@interfaces/application' 9 | import { AppConfig } from '@interfaces/types/config' 10 | 11 | export async function getApp(): Promise> { 12 | const app = new Application>('Gateway') 13 | 14 | await app.setConfig(config) 15 | 16 | await app.setDeps(deps) 17 | await app.loadDepsFromFolder({ folderName: 'middlewares', nameFormatter: (name: string) => `${name}Middleware` }) 18 | 19 | return await app.initialize() 20 | } 21 | -------------------------------------------------------------------------------- /tests/mocks/middlewares/fileUpload.ts: -------------------------------------------------------------------------------- 1 | import { MimeType } from '@interfaces/index' 2 | import { AppRoute, FileSize } from '@interfaces/routes/appRoute' 3 | 4 | export const validAppRoutes = { 5 | single: { 6 | upload: { 7 | allowedMimeTypes: [MimeType.PDF], 8 | required: true, 9 | field: 'file', 10 | maxFileSize: FileSize.KB_500, 11 | attempts: { max: 2, periodSec: 10 }, 12 | }, 13 | }, 14 | multiple: { 15 | upload: { 16 | allowedMimeTypes: [MimeType.PDF], 17 | required: true, 18 | fields: [{ name: 'file1', maxCount: 1 }, { name: 'file2' }], 19 | maxFileSize: FileSize.KB_500, 20 | attempts: { max: 2, periodSec: 10 }, 21 | }, 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /src/dataMappers/cms/faqCategoryCmsDataMapper.ts: -------------------------------------------------------------------------------- 1 | import FaqCmsDataMapper from '@dataMappers/cms/faqCmsDataMapper' 2 | 3 | import { CmsFaqCategory } from '@interfaces/models/cms/faq' 4 | import { FaqCategory } from '@interfaces/models/faqCategory' 5 | 6 | export default class FaqCategoryCmsDataMapper { 7 | constructor(private readonly faqCmsDataMapper: FaqCmsDataMapper) {} 8 | 9 | toEntity(item: CmsFaqCategory): FaqCategory { 10 | const { code, name, faq, features, order, sessionType } = item 11 | 12 | return { 13 | code, 14 | name, 15 | sessionType, 16 | faq: faq.data.map((faqItem) => this.faqCmsDataMapper.toEntity(faqItem.attributes)), 17 | features: features?.map(({ value }) => value), 18 | order, 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/unit/actions/v1/getUserIdentifier.spec.ts: -------------------------------------------------------------------------------- 1 | import TestKit from '@diia-inhouse/test' 2 | 3 | import GetUserIdentifierAction from '@actions/v1/getUserIdentifier' 4 | 5 | describe(`Action ${GetUserIdentifierAction.constructor.name}`, () => { 6 | const testKit = new TestKit() 7 | const getUserIdentifierAction = new GetUserIdentifierAction() 8 | 9 | describe('Method `handler`', () => { 10 | it('should successfully return user identifier', async () => { 11 | const headers = testKit.session.getHeaders() 12 | 13 | const args = { 14 | headers, 15 | session: testKit.session.getUserSession(), 16 | } 17 | 18 | expect(await getUserIdentifierAction.handler(args)).toEqual({ identifier: args.session.user.identifier }) 19 | }) 20 | }) 21 | }) 22 | -------------------------------------------------------------------------------- /src/actions/v1/getAppSettings.ts: -------------------------------------------------------------------------------- 1 | import { AppAction } from '@diia-inhouse/diia-app' 2 | 3 | import { ActionVersion, SessionType } from '@diia-inhouse/types' 4 | 5 | import SettingsService from '@services/settings' 6 | 7 | import { ActionResult, CustomActionArguments } from '@interfaces/actions/v1/getAppSettings' 8 | 9 | export default class GetAppSettingsAction implements AppAction { 10 | constructor(private readonly settingsService: SettingsService) {} 11 | 12 | readonly sessionType: SessionType = SessionType.None 13 | 14 | readonly actionVersion: ActionVersion = ActionVersion.V1 15 | 16 | readonly name: string = 'getAppSettings' 17 | 18 | async handler(args: CustomActionArguments): Promise { 19 | const { headers } = args 20 | 21 | return await this.settingsService.getAppSettings(headers) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/services/notification.ts: -------------------------------------------------------------------------------- 1 | import { MoleculerService } from '@diia-inhouse/diia-app' 2 | 3 | import { ActionSession, ActionVersion, GenericObject } from '@diia-inhouse/types' 4 | 5 | export default class NotificationService { 6 | private readonly serviceName: string = 'Notification' 7 | 8 | constructor(private readonly lazyMoleculer: () => MoleculerService) {} 9 | 10 | async makeRequest(actionName: string, params: GenericObject, session: ActionSession): Promise { 11 | return await this.lazyMoleculer().act(this.serviceName, { name: actionName, actionVersion: ActionVersion.V1 }, { params, session }) 12 | } 13 | 14 | async hasPushToken(): Promise<{ hasPushToken: boolean }> { 15 | return await this.lazyMoleculer().act(this.serviceName, { name: 'hasPushToken', actionVersion: ActionVersion.V1 }) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/actions/v1/testAcquirerProviderEncodedData.ts: -------------------------------------------------------------------------------- 1 | import { AppAction } from '@diia-inhouse/diia-app' 2 | 3 | import { ActionVersion, Logger, SessionType } from '@diia-inhouse/types' 4 | 5 | import { ActionResult } from '@interfaces/actions/v1/testAcquirerProviderEncodedData' 6 | 7 | export default class TestAcquirerProviderEncodedDataAction implements AppAction { 8 | constructor(private readonly logger: Logger) {} 9 | 10 | readonly sessionType: SessionType = SessionType.None 11 | 12 | readonly actionVersion: ActionVersion = ActionVersion.V1 13 | 14 | readonly name: string = 'testAcquirerProviderEncodedData' 15 | 16 | async handler(): Promise { 17 | this.logger.debug('Receive encoded data from Diia app in sign hashed files flow') 18 | 19 | const success = true 20 | 21 | return { success } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /migrations/20240313113029-add-e-resident-error-templates.ts: -------------------------------------------------------------------------------- 1 | import 'module-alias/register' 2 | import { readFileSync } from 'fs' 3 | import { resolve } from 'path' 4 | 5 | import { Db } from 'mongodb' 6 | 7 | import { ErrorTemplate } from '@interfaces/models/errorTemplate' 8 | 9 | const collectionName = 'errortemplates' 10 | const filepath: string = resolve('eResidentErrorTemplates.json') 11 | 12 | export async function up(db: Db): Promise { 13 | const errorTemplatesStr = await readFileSync(filepath).toString() 14 | try { 15 | const errorTemplates: ErrorTemplate[] = JSON.parse(errorTemplatesStr) 16 | const operations = errorTemplates.map((errorTemplate) => ({ 17 | insertOne: { document: errorTemplate }, 18 | })) 19 | 20 | await db.collection(collectionName).bulkWrite(operations) 21 | } catch (err) {} 22 | } 23 | -------------------------------------------------------------------------------- /src/validation/sandbox.ts: -------------------------------------------------------------------------------- 1 | import { ValidationPredicate } from '@interfaces/validation/sandbox' 2 | 3 | export class ValidationSandBox { 4 | private error: Error | undefined 5 | 6 | private nextValidator: ValidationSandBox | null = null 7 | 8 | constructor(private readonly predicate: ValidationPredicate) {} 9 | 10 | next(validator: ValidationSandBox): ValidationSandBox { 11 | this.nextValidator = validator 12 | 13 | return this 14 | } 15 | 16 | validate(): Error | undefined { 17 | this.error = this.predicate() 18 | 19 | if (!this.error && this.nextValidator) { 20 | return this.nextValidator.validate() 21 | } 22 | 23 | return this.error 24 | } 25 | 26 | static build(predicate: ValidationPredicate): ValidationSandBox { 27 | return new ValidationSandBox(predicate) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import { Application, ServiceContext } from '@diia-inhouse/diia-app' 2 | 3 | import configFactory from '@src/config' 4 | import deps from '@src/deps' 5 | 6 | import { AppDeps } from '@interfaces/application' 7 | import { AppConfig } from '@interfaces/types/config' 8 | 9 | export async function bootstrap(serviceName: string): Promise { 10 | const app = new Application>(serviceName) 11 | 12 | await app.setConfig(configFactory) 13 | 14 | await app.setDeps(deps) 15 | await app.loadDepsFromFolder({ folderName: 'middlewares', nameFormatter: (name: string) => `${name}Middleware` }) 16 | 17 | const { config, start, container } = await app.initialize() 18 | 19 | await start() 20 | 21 | if (config.swagger.isEnabled) { 22 | container.resolve('openApiGenerator').generateOpenApiSchemas() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/interfaces/models/faqCategory.ts: -------------------------------------------------------------------------------- 1 | import { Document } from '@diia-inhouse/db' 2 | import { ProfileFeature, SessionType } from '@diia-inhouse/types' 3 | 4 | export enum FaqParameterType { 5 | Link = 'link', 6 | Phone = 'phone', 7 | Email = 'email', 8 | } 9 | 10 | export interface FaqParameter { 11 | type: FaqParameterType 12 | data: { 13 | name: string 14 | alt: string 15 | resource: string 16 | } 17 | } 18 | 19 | export interface FaqItem { 20 | question: string 21 | answer: string 22 | parameters: FaqParameter[] 23 | subFeatures: string[] 24 | } 25 | 26 | export interface FaqCategory { 27 | code: string 28 | name: string 29 | sessionType: SessionType 30 | faq: FaqItem[] 31 | features?: ProfileFeature[] 32 | order: number 33 | } 34 | 35 | export interface FaqCategoryModel extends FaqCategory, Document { 36 | createdAt?: Date 37 | } 38 | -------------------------------------------------------------------------------- /tests/unit/actions/v1/bankIdCallback.spec.ts: -------------------------------------------------------------------------------- 1 | import TestKit from '@diia-inhouse/test' 2 | 3 | import BankIdCallbackAction from '@actions/v1/bankIdCallback' 4 | 5 | import { generateUuid } from '@mocks/randomData' 6 | 7 | describe(`Action ${BankIdCallbackAction.constructor.name}`, () => { 8 | const testKit = new TestKit() 9 | const bankIdCallbackAction = new BankIdCallbackAction() 10 | 11 | describe('Method `handler`', () => { 12 | it('should successfully return params object', async () => { 13 | const headers = testKit.session.getHeaders() 14 | 15 | const args = { 16 | params: { 17 | state: generateUuid(), 18 | }, 19 | code: generateUuid(), 20 | headers, 21 | } 22 | 23 | expect(await bankIdCallbackAction.handler(args)).toEqual(args.params) 24 | }) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /tests/unit/actions/v1/testAcquirerProviderEncodedData.spec.ts: -------------------------------------------------------------------------------- 1 | import DiiaLogger from '@diia-inhouse/diia-logger' 2 | import { mockClass } from '@diia-inhouse/test' 3 | 4 | import TestAcquirerProviderEncodedDataAction from '@actions/v1/testAcquirerProviderEncodedData' 5 | 6 | const DiiaLoggerMock = mockClass(DiiaLogger) 7 | 8 | describe(`Action ${TestAcquirerProviderEncodedDataAction.constructor.name}`, () => { 9 | const logger = new DiiaLoggerMock() 10 | const testAcquirerProviderEncodedDataAction = new TestAcquirerProviderEncodedDataAction(logger) 11 | 12 | describe('Method `handler`', () => { 13 | it('should return success true', async () => { 14 | expect(await testAcquirerProviderEncodedDataAction.handler()).toEqual({ success: true }) 15 | expect(logger.debug).toHaveBeenLastCalledWith('Receive encoded data from Diia app in sign hashed files flow') 16 | }) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /src/actions/v1/faq/getFaq.ts: -------------------------------------------------------------------------------- 1 | import { AppAction } from '@diia-inhouse/diia-app' 2 | 3 | import { ActionVersion, SessionType } from '@diia-inhouse/types' 4 | import { UserActionArguments } from '@diia-inhouse/types/dist/types/actions/actionArguments' 5 | 6 | import FaqService from '@services/faq' 7 | 8 | import { ActionResult } from '@interfaces/actions/v1/getFaq' 9 | 10 | export default class GetFaqAction implements AppAction { 11 | constructor(private readonly faqService: FaqService) {} 12 | 13 | readonly sessionType: SessionType = SessionType.User 14 | 15 | readonly actionVersion: ActionVersion = ActionVersion.V1 16 | 17 | readonly name: string = 'getFaq' 18 | 19 | async handler(args: UserActionArguments): Promise { 20 | const { 21 | session: { sessionType, features = {} }, 22 | } = args 23 | 24 | return await this.faqService.getFaq(sessionType, features) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/actions/v1/getAppVersion.ts: -------------------------------------------------------------------------------- 1 | import { AppAction } from '@diia-inhouse/diia-app' 2 | 3 | import { ActionVersion, SessionType } from '@diia-inhouse/types' 4 | 5 | import VersionService from '@services/version' 6 | 7 | import { ActionResult, CustomActionArguments } from '@interfaces/actions/v1/getAppVersion' 8 | 9 | export default class GetAppVersionAction implements AppAction { 10 | constructor(private readonly versionService: VersionService) {} 11 | 12 | readonly sessionType: SessionType = SessionType.None 13 | 14 | readonly actionVersion: ActionVersion = ActionVersion.V1 15 | 16 | readonly name: string = 'getAppVersion' 17 | 18 | async handler(args: CustomActionArguments): Promise { 19 | const { 20 | headers: { platformType }, 21 | } = args 22 | 23 | const minVersion = this.versionService.getMinAppVersion(platformType) 24 | 25 | return { minVersion } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/dataMappers/cms/faqCmsDataMapper.ts: -------------------------------------------------------------------------------- 1 | import { CmsFaq } from '@interfaces/models/cms/faq' 2 | import { FaqItem } from '@interfaces/models/faqCategory' 3 | 4 | export default class FaqCmsDataMapper { 5 | toEntity(item: CmsFaq): FaqItem { 6 | const { question, answer, parameters } = item 7 | 8 | return { 9 | question, 10 | answer, 11 | subFeatures: [], 12 | parameters: parameters.map((parameterItem) => { 13 | const { 14 | type, 15 | data: { name: parameterName, alt, resource }, 16 | } = parameterItem 17 | 18 | return { 19 | type, 20 | data: { 21 | name: parameterName, 22 | alt, 23 | resource, 24 | }, 25 | } 26 | }), 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /cabinetProcessCodesTemplates/auth.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "processCode": 210401005, 4 | "template": { 5 | "type": "executeAction", 6 | "data": { 7 | "action": "getMethods" 8 | } 9 | } 10 | }, 11 | { 12 | "processCode": 210401006, 13 | "template": { 14 | "type": "executeAction", 15 | "data": { 16 | "action": "getMethods" 17 | } 18 | } 19 | }, 20 | { 21 | "processCode": 210401007, 22 | "template": { 23 | "type": "executeAction", 24 | "data": { 25 | "action": "getMethods" 26 | } 27 | } 28 | }, 29 | { 30 | "processCode": 210401009, 31 | "template": { 32 | "type": "executeAction", 33 | "data": { 34 | "action": "getToken" 35 | } 36 | } 37 | } 38 | ] 39 | -------------------------------------------------------------------------------- /src/actions/v1/getPublicServices.ts: -------------------------------------------------------------------------------- 1 | import { AppAction } from '@diia-inhouse/diia-app' 2 | 3 | import { ActionVersion, SessionType } from '@diia-inhouse/types' 4 | 5 | import PublicServicesListService from '@services/publicServicesList' 6 | 7 | import { ActionResult } from '@interfaces/actions/v1/getPublicServices' 8 | import { PublicServiceResponse } from '@interfaces/services/publicServicesList' 9 | 10 | export default class GetPublicServicesAction implements AppAction { 11 | constructor(private readonly publicServicesListService: PublicServicesListService) {} 12 | 13 | readonly sessionType: SessionType = SessionType.User 14 | 15 | readonly actionVersion: ActionVersion = ActionVersion.V1 16 | 17 | readonly name: string = 'getPublicServices' 18 | 19 | async handler(): Promise { 20 | const publicServices: PublicServiceResponse[] = await this.publicServicesListService.getPublicServices() 21 | 22 | return { publicServices } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/actions/v1/bankIdCallback.ts: -------------------------------------------------------------------------------- 1 | import { AppAction } from '@diia-inhouse/diia-app' 2 | 3 | import { ActionVersion, SessionType } from '@diia-inhouse/types' 4 | import { ValidationSchema } from '@diia-inhouse/validators' 5 | 6 | import { CustomActionArguments } from '@interfaces/actions/v1/bankIdCallback' 7 | 8 | export default class BankIdCallbackAction implements AppAction { 9 | readonly sessionType: SessionType = SessionType.None 10 | 11 | readonly actionVersion: ActionVersion = ActionVersion.V1 12 | 13 | readonly name: string = 'bankIdCallback' 14 | 15 | readonly validationRules: ValidationSchema = { 16 | code: { type: 'string' }, 17 | state: { type: 'string', optional: true }, 18 | error: { type: 'string', optional: true }, 19 | error_description: { type: 'string', optional: true }, 20 | } 21 | 22 | async handler(args: CustomActionArguments): Promise { 23 | return args.params 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/actions/v1/getEResidentAppVersion.ts: -------------------------------------------------------------------------------- 1 | import { AppAction } from '@diia-inhouse/diia-app' 2 | 3 | import { ActionVersion, SessionType } from '@diia-inhouse/types' 4 | 5 | import VersionService from '@services/version' 6 | 7 | import { ActionResult, CustomActionArguments } from '@interfaces/actions/v1/getAppVersion' 8 | import { MinAppVersionConfigType } from '@interfaces/services/version' 9 | 10 | export default class GetEResidentAppVersion implements AppAction { 11 | constructor(private readonly versionService: VersionService) {} 12 | 13 | readonly sessionType: SessionType = SessionType.None 14 | 15 | readonly actionVersion: ActionVersion = ActionVersion.V1 16 | 17 | readonly name: string = 'getEResidentAppVersion' 18 | 19 | async handler(args: CustomActionArguments): Promise { 20 | const minVersion = this.versionService.getMinAppVersion(args.headers.platformType, MinAppVersionConfigType.MinEResidentAppVersion) 21 | 22 | return { minVersion } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /migrations/20240202131000-update-e-resident-faq.ts: -------------------------------------------------------------------------------- 1 | import 'module-alias/register' 2 | import { readFileSync } from 'fs' 3 | import { resolve } from 'path' 4 | 5 | import { Db } from 'mongodb' 6 | 7 | import { SessionType } from '@diia-inhouse/types' 8 | 9 | import { FaqCategory } from '@interfaces/models/faqCategory' 10 | 11 | const collectionName = 'faqcategories' 12 | const filepath: string = resolve('eResidentFaq.json') 13 | 14 | export async function up(db: Db): Promise { 15 | const categoriesStr = await readFileSync(filepath).toString() 16 | try { 17 | const categories: FaqCategory[] = JSON.parse(categoriesStr).categories 18 | 19 | const operations = categories.map(({ code, name, faq }) => ({ 20 | updateOne: { 21 | filter: { code, sessionType: SessionType.EResident }, 22 | update: { 23 | $set: { name, faq }, 24 | }, 25 | }, 26 | })) 27 | 28 | await db.collection(collectionName).bulkWrite(operations) 29 | } catch (err) {} 30 | } 31 | -------------------------------------------------------------------------------- /tests/unit/actions/v1/bankIdAuthCodeResult.spec.ts: -------------------------------------------------------------------------------- 1 | import DiiaLogger from '@diia-inhouse/diia-logger' 2 | import TestKit, { mockClass } from '@diia-inhouse/test' 3 | 4 | import BankIdAuthCodeResultAction from '@actions/v1/bankIdAuthCodeResult' 5 | 6 | const DiiaLoggerMock = mockClass(DiiaLogger) 7 | 8 | describe(`Action ${BankIdAuthCodeResultAction.constructor.name}`, () => { 9 | const testKit = new TestKit() 10 | const logger = new DiiaLoggerMock() 11 | const bankIdAuthCodeResultAction = new BankIdAuthCodeResultAction(logger) 12 | 13 | describe('Method `handler`', () => { 14 | it('should successfully return {}', async () => { 15 | const headers = testKit.session.getHeaders() 16 | 17 | expect( 18 | await bankIdAuthCodeResultAction.handler({ 19 | params: {}, 20 | code: 'code', 21 | headers, 22 | }), 23 | ).toEqual({}) 24 | expect(logger.debug).toHaveBeenCalledWith('Received auth code result data', {}) 25 | }) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@diia-inhouse/configs/tsconfig", 3 | "compilerOptions": { 4 | "plugins": [ 5 | { 6 | "transform": "@diia-inhouse/diia-app/dist/plugins/openapi" 7 | } 8 | ], 9 | "outDir": "dist", 10 | "declaration": true, 11 | "declarationDir": "dist/types", 12 | "baseUrl": "./", 13 | "strict": true, 14 | "skipLibCheck": true, 15 | "lib": ["es2023", "DOM"], 16 | "paths": { 17 | "@services/*": ["src/services/*"], 18 | "@interfaces/*": ["src/interfaces/*"], 19 | "@models/*": ["src/models/*"], 20 | "@dataMappers/*": ["src/dataMappers/*"], 21 | "@utils/*": ["src/utils/*"], 22 | "@xmlMappings/*": ["src/xmlMappings/*"], 23 | "@actions/*": ["src/actions/*"], 24 | "@src/*": ["src/*"], 25 | "@mocks/*": ["tests/mocks/*"], 26 | "@validation/*": ["src/validation/*"], 27 | "@tests/*": ["tests/*"] 28 | } 29 | }, 30 | "include": ["src/**/*"] 31 | } 32 | -------------------------------------------------------------------------------- /tests/unit/providers/faq/mongodbFaqProvider.spec.ts: -------------------------------------------------------------------------------- 1 | import { SessionType } from '@diia-inhouse/types' 2 | 3 | import { MongodbFaqProvider } from '@src/providers/faq' 4 | 5 | const mockedCategories = [ 6 | { 7 | code: '1', 8 | name: 'Category 1', 9 | sessionType: 'session', 10 | faq: [], 11 | features: ['feature1'], 12 | order: 1, 13 | }, 14 | ] 15 | 16 | jest.mock('@models/faqCategory', () => ({ 17 | __esModule: true, 18 | default: { 19 | aggregate: (): unknown => ({ 20 | sort: () => ({ 21 | exec: () => mockedCategories, 22 | }), 23 | }), 24 | }, 25 | })) 26 | 27 | describe('mongodbFaqProvider', () => { 28 | const mongodbFaqProvider = new MongodbFaqProvider() 29 | 30 | describe('getList', () => { 31 | it('should return list from model', async () => { 32 | const result = await mongodbFaqProvider.getCategoriesList(SessionType.User, {}) 33 | 34 | expect(result).toStrictEqual(mockedCategories) 35 | }) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /src/actions/v1/bankIdAuthCodeResult.ts: -------------------------------------------------------------------------------- 1 | import { AppAction } from '@diia-inhouse/diia-app' 2 | 3 | import { ActionVersion, Logger, SessionType } from '@diia-inhouse/types' 4 | import { ValidationSchema } from '@diia-inhouse/validators' 5 | 6 | import { CustomActionArguments } from '@interfaces/actions/v1/bankIdAuthCodeResult' 7 | 8 | export default class BankIdAuthCodeResultAction implements AppAction { 9 | constructor(private readonly logger: Logger) {} 10 | 11 | readonly sessionType: SessionType = SessionType.None 12 | 13 | readonly actionVersion: ActionVersion = ActionVersion.V1 14 | 15 | readonly name: string = 'bankIdAuthCodeResult' 16 | 17 | readonly validationRules: ValidationSchema = { 18 | code: { type: 'string' }, 19 | state: { type: 'string', optional: true }, 20 | error: { type: 'string', optional: true }, 21 | error_description: { type: 'string', optional: true }, 22 | } 23 | 24 | async handler(args: CustomActionArguments): Promise> { 25 | this.logger.debug('Received auth code result data', args.params) 26 | 27 | return {} 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /processCodesTemplates/payment.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "processCode": 24101001, 4 | "template": { 5 | "type": "middleCenterAlignAlert", 6 | "isClosable": true, 7 | "data": { 8 | "icon": "😞", 9 | "title": "Квитанцію не завантажено", 10 | "description": "Спробуйте, будь ласка, пізніше або зверніться до служби підтримки ТОВ «ФК «Єдиний простір» за тел.+38 (044) 344-56-47 або напишіть на support@onespace.in.ua.", 11 | "mainButton": { 12 | "name": "Спробувати ще", 13 | "action": "repeat" 14 | } 15 | } 16 | } 17 | }, 18 | { 19 | "processCode": 24101002, 20 | "template": { 21 | "type": "smallAlert", 22 | "isClosable": false, 23 | "data": { 24 | "icon": "😞", 25 | "title": "Невідомий запит на отримання квитанцій", 26 | "mainButton": { 27 | "name": "Вийти", 28 | "action": "skip" 29 | } 30 | } 31 | } 32 | } 33 | ] 34 | -------------------------------------------------------------------------------- /src/routes/documents/user.ts: -------------------------------------------------------------------------------- 1 | import { ActionVersion, HttpMethod, SessionType } from '@diia-inhouse/types' 2 | 3 | import { RouteHeaderRawName } from '@interfaces/index' 4 | import { AppRoute } from '@interfaces/routes/appRoute' 5 | 6 | enum DocumentsActions { 7 | GetUserByITN = 'getUserByITN', 8 | } 9 | 10 | const routes: AppRoute[] = [ 11 | { 12 | method: HttpMethod.GET, 13 | path: '/api/:apiVersion/documents/itn', 14 | action: DocumentsActions.GetUserByITN, 15 | auth: [{ sessionType: SessionType.User, version: ActionVersion.V1 }], 16 | headers: [ 17 | { name: RouteHeaderRawName.MOBILE_UID, versions: [ActionVersion.V1] }, 18 | { name: RouteHeaderRawName.TOKEN, versions: [ActionVersion.V1] }, 19 | { name: RouteHeaderRawName.APP_VERSION, versions: [ActionVersion.V1] }, 20 | { name: RouteHeaderRawName.PLATFORM_TYPE, versions: [ActionVersion.V1] }, 21 | { name: RouteHeaderRawName.PLATFORM_VERSION, versions: [ActionVersion.V1] }, 22 | ], 23 | metadata: { 24 | tags: ['ITN'], 25 | }, 26 | }, 27 | ] 28 | 29 | export default routes 30 | -------------------------------------------------------------------------------- /src/routes/feed.ts: -------------------------------------------------------------------------------- 1 | import { HttpMethod } from '@diia-inhouse/types' 2 | 3 | import { DEFAULT_USER_AUTH, DEFAULT_USER_HEADERS } from '@src/routes/defaults' 4 | 5 | import { AppRoute } from '@interfaces/routes/appRoute' 6 | 7 | const serviceId = 'feed' 8 | 9 | const routes: AppRoute[] = [ 10 | { 11 | method: HttpMethod.GET, 12 | path: '/api/v1/feed', 13 | proxyTo: { serviceId }, 14 | auth: DEFAULT_USER_AUTH, 15 | headers: DEFAULT_USER_HEADERS, 16 | }, 17 | { 18 | method: HttpMethod.GET, 19 | path: '/api/v1/feed/news/screen', 20 | proxyTo: { serviceId }, 21 | auth: DEFAULT_USER_AUTH, 22 | headers: DEFAULT_USER_HEADERS, 23 | }, 24 | { 25 | method: HttpMethod.GET, 26 | path: '/api/v1/feed/news', 27 | proxyTo: { serviceId }, 28 | auth: DEFAULT_USER_AUTH, 29 | headers: DEFAULT_USER_HEADERS, 30 | }, 31 | { 32 | method: HttpMethod.GET, 33 | path: '/api/v1/feed/news/:id', 34 | proxyTo: { serviceId }, 35 | auth: DEFAULT_USER_AUTH, 36 | headers: DEFAULT_USER_HEADERS, 37 | }, 38 | ] 39 | 40 | export default routes 41 | -------------------------------------------------------------------------------- /tests/unit/actions/v1/getAppVersion.spec.ts: -------------------------------------------------------------------------------- 1 | import TestKit, { mockClass } from '@diia-inhouse/test' 2 | 3 | import GetAppVersionAction from '@actions/v1/getAppVersion' 4 | 5 | import VersionService from '@services/version' 6 | 7 | import { AppConfig } from '@interfaces/types/config' 8 | 9 | const VersionServiceMock = mockClass(VersionService) 10 | 11 | describe(`Action ${GetAppVersionAction.constructor.name}`, () => { 12 | const testKit = new TestKit() 13 | const versionService = new VersionServiceMock({}) 14 | const getAppVersionAction = new GetAppVersionAction(versionService) 15 | 16 | describe('Method `handler`', () => { 17 | it('should successfully return min app version', async () => { 18 | const headers = testKit.session.getHeaders() 19 | 20 | const args = { 21 | headers, 22 | } 23 | 24 | jest.spyOn(versionService, 'getMinAppVersion').mockReturnValueOnce('1.0.0') 25 | 26 | expect(await getAppVersionAction.handler(args)).toEqual({ minVersion: '1.0.0' }) 27 | expect(versionService.getMinAppVersion).toHaveBeenCalledWith(headers.platformType) 28 | }) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /tests/unit/services/partner.spec.ts: -------------------------------------------------------------------------------- 1 | import { MoleculerService } from '@diia-inhouse/diia-app' 2 | 3 | import PartnerService from '@src/services/partner' 4 | 5 | import { GetPartnerByTokenResult } from '@interfaces/services/partner' 6 | 7 | describe('PartnerService', () => { 8 | const mockAct = jest.fn() 9 | const lazyMoleculer = (): MoleculerService => ({ 10 | act: mockAct, 11 | }) 12 | 13 | const partnerService = new PartnerService(lazyMoleculer) 14 | 15 | beforeEach(() => { 16 | jest.clearAllMocks() 17 | }) 18 | 19 | it('should get partner by token', async () => { 20 | const partnerToken = 'partner-token-123' 21 | const expectedResult: GetPartnerByTokenResult = {} 22 | 23 | mockAct.mockResolvedValue(expectedResult) 24 | 25 | const result = await partnerService.getPartnerByToken(partnerToken) 26 | 27 | expect(mockAct).toHaveBeenCalledTimes(1) 28 | expect(mockAct).toHaveBeenCalledWith('Partner', { name: 'getPartnerByToken', actionVersion: 'v1' }, { params: { partnerToken } }) 29 | 30 | expect(result).toEqual(expectedResult) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/actions/v1/faq/createFaq.ts: -------------------------------------------------------------------------------- 1 | import { AppAction } from '@diia-inhouse/diia-app' 2 | 3 | import { ActionVersion, SessionType } from '@diia-inhouse/types' 4 | import { ValidationSchema } from '@diia-inhouse/validators' 5 | 6 | import { getFaqCategoryRule } from '@src/validationRules/faqCategory' 7 | 8 | import FaqService from '@services/faq' 9 | 10 | import { ActionResult, CustomActionArguments } from '@interfaces/actions/v1/faq/createFaq' 11 | 12 | export default class CreateFaqAction implements AppAction { 13 | constructor(private readonly faqService: FaqService) {} 14 | 15 | readonly sessionType: SessionType = SessionType.Partner 16 | 17 | readonly actionVersion: ActionVersion = ActionVersion.V1 18 | 19 | readonly name: string = 'createFaq' 20 | 21 | readonly validationRules: ValidationSchema = { 22 | session: { type: 'string', enum: Object.values(SessionType) }, 23 | categories: { 24 | type: 'array', 25 | items: getFaqCategoryRule(), 26 | }, 27 | } 28 | 29 | async handler(args: CustomActionArguments): Promise { 30 | const { params: faq } = args 31 | 32 | return await this.faqService.createFaq(faq) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/actions/v1/faq/updateFaq.ts: -------------------------------------------------------------------------------- 1 | import { AppAction } from '@diia-inhouse/diia-app' 2 | 3 | import { ActionVersion, SessionType } from '@diia-inhouse/types' 4 | import { ValidationSchema } from '@diia-inhouse/validators' 5 | 6 | import { getFaqCategoryRule } from '@src/validationRules/faqCategory' 7 | 8 | import FaqService from '@services/faq' 9 | 10 | import { ActionResult, CustomActionArguments } from '@interfaces/actions/v1/faq/updateFaq' 11 | 12 | export default class UpdateFaqAction implements AppAction { 13 | constructor(private readonly faqService: FaqService) {} 14 | 15 | readonly sessionType: SessionType = SessionType.Partner 16 | 17 | readonly actionVersion: ActionVersion = ActionVersion.V1 18 | 19 | readonly name: string = 'updateFaq' 20 | 21 | readonly validationRules: ValidationSchema = { 22 | session: { type: 'string', enum: Object.values(SessionType) }, 23 | categories: { 24 | type: 'array', 25 | items: getFaqCategoryRule(false), 26 | }, 27 | } 28 | 29 | async handler(args: CustomActionArguments): Promise { 30 | const { params: service } = args 31 | 32 | return await this.faqService.replaceFaq(service) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/actions/v1/errorTemplate/getErrorTemplateByErrorCode.ts: -------------------------------------------------------------------------------- 1 | import { AppAction } from '@diia-inhouse/diia-app' 2 | 3 | import { ActionVersion, SessionType } from '@diia-inhouse/types' 4 | import { ValidationSchema } from '@diia-inhouse/validators' 5 | 6 | import ErrorTemplateService from '@services/errorTemplate' 7 | 8 | import { ActionResult, CustomActionArguments } from '@interfaces/actions/v1/errorTemplates/getErrorTemplateByErrorCode' 9 | 10 | export default class GetErrorTemplateByErrorCodeAction implements AppAction { 11 | constructor(private readonly errorTemplateService: ErrorTemplateService) {} 12 | 13 | readonly sessionType: SessionType = SessionType.Partner 14 | 15 | readonly actionVersion: ActionVersion = ActionVersion.V1 16 | 17 | readonly name: string = 'getErrorTemplateByErrorCode' 18 | 19 | readonly validationRules: ValidationSchema = { 20 | errorCode: { type: 'number', convert: true }, 21 | } 22 | 23 | async handler(args: CustomActionArguments): Promise { 24 | const { 25 | params: { errorCode }, 26 | } = args 27 | 28 | return await this.errorTemplateService.fetchErrorTemplateByCode(errorCode) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/actions/v1/errorTemplate/getErrorTemplatesList.ts: -------------------------------------------------------------------------------- 1 | import { AppAction } from '@diia-inhouse/diia-app' 2 | 3 | import { ActionVersion, SessionType } from '@diia-inhouse/types' 4 | import { ValidationSchema } from '@diia-inhouse/validators' 5 | 6 | import ErrorTemplateService from '@services/errorTemplate' 7 | 8 | import { ActionResult, CustomActionArguments } from '@interfaces/actions/v1/errorTemplates/getErrorTemplatesList' 9 | 10 | export default class GetErrorTemplatesListAction implements AppAction { 11 | constructor(private readonly errorTemplateService: ErrorTemplateService) {} 12 | 13 | readonly sessionType: SessionType = SessionType.Partner 14 | 15 | readonly actionVersion: ActionVersion = ActionVersion.V1 16 | 17 | readonly name: string = 'getErrorTemplatesList' 18 | 19 | readonly validationRules: ValidationSchema = { 20 | skip: { type: 'number', optional: true }, 21 | limit: { type: 'number', optional: true }, 22 | } 23 | 24 | async handler(args: CustomActionArguments): Promise { 25 | const { 26 | params: { skip = 0, limit = 100 }, 27 | } = args 28 | 29 | return await this.errorTemplateService.getErrorTemplatesList({ skip, limit }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/actions/v1/unauthorizedSimulate.ts: -------------------------------------------------------------------------------- 1 | import { AppAction } from '@diia-inhouse/diia-app' 2 | 3 | import { UnauthorizedError } from '@diia-inhouse/errors' 4 | import { CacheService } from '@diia-inhouse/redis' 5 | import { ActionVersion, SessionType } from '@diia-inhouse/types' 6 | 7 | import { ActionResult, CustomActionArguments } from '@interfaces/actions/v1/unauthorizedSimulate' 8 | 9 | export default class UnauthorizedSimulateAction implements AppAction { 10 | readonly cachePrefix = 'unauth_emul_' 11 | 12 | constructor(private readonly cache: CacheService) {} 13 | 14 | readonly sessionType: SessionType = SessionType.User 15 | 16 | readonly actionVersion: ActionVersion = ActionVersion.V1 17 | 18 | readonly name: string = 'unauthorizedSimulate' 19 | 20 | async handler(args: CustomActionArguments): Promise { 21 | const redisKey = this.cachePrefix + args.session.user.mobileUid 22 | const isUnauth = (await this.cache.get(redisKey)) === '1' 23 | 24 | if (isUnauth) { 25 | await this.cache.set(redisKey, '0') 26 | throw new UnauthorizedError() 27 | } 28 | 29 | await this.cache.set(redisKey, '1') 30 | 31 | return { status: 'ok' } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/unit/providers/faq/strapiFaqProvider.spec.ts: -------------------------------------------------------------------------------- 1 | import { CmsEntriesMeta, CmsService, StrapiConfig } from '@diia-inhouse/cms' 2 | import { HttpService } from '@diia-inhouse/http' 3 | import { mockClass } from '@diia-inhouse/test' 4 | import { Logger, SessionType } from '@diia-inhouse/types' 5 | 6 | import { StrapiFaqProvider } from '@src/providers/faq' 7 | 8 | import FaqCategoryCmsDataMapper from '@dataMappers/cms/faqCategoryCmsDataMapper' 9 | 10 | describe('strapiFaqProvider', () => { 11 | const resultData = { 12 | meta: '', 13 | data: [], 14 | } 15 | 16 | const cmsService = new (mockClass(CmsService))({}, {}, {}) 17 | const faqCategoryCmsDataMapper = {} 18 | 19 | const strapiFaqProvider = new StrapiFaqProvider(cmsService, faqCategoryCmsDataMapper) 20 | 21 | describe('method: `getCategoriesList`', () => { 22 | it('should return categories list', async () => { 23 | jest.spyOn(cmsService, 'getList').mockResolvedValueOnce(resultData) 24 | const list = await strapiFaqProvider.getCategoriesList({}, {}) 25 | 26 | expect(list).toStrictEqual(resultData.data) 27 | }) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /migrations/20230221142216-category-separate-doc.ts: -------------------------------------------------------------------------------- 1 | import 'module-alias/register' 2 | 3 | import { Db } from 'mongodb' 4 | 5 | import { SessionType } from '@diia-inhouse/types' 6 | 7 | import { FaqCategory } from '@interfaces/models/faqCategory' 8 | 9 | const collectionName = 'faqs' 10 | const categoryCollectionName = 'faqcategories' 11 | 12 | export async function up(db: Db): Promise { 13 | const faqsCollection = db.collection(collectionName) 14 | const faqsCategoryCollection = db.collection(categoryCollectionName) 15 | const allFaqs = faqsCollection.find() 16 | 17 | const categoriesToInsert: FaqCategory[] = [] 18 | 19 | let i = 0 20 | await allFaqs.forEach((faq) => { 21 | const categories = faq.categories 22 | let j = i * 1000 + 1 23 | for (const category of categories) { 24 | categoriesToInsert.push({ 25 | code: category.code, 26 | name: category.name, 27 | faq: category.faq, 28 | sessionType: faq.session, 29 | features: [], 30 | order: j, 31 | }) 32 | 33 | j += 1 34 | } 35 | 36 | i += 1 37 | }) 38 | 39 | await faqsCategoryCollection.insertMany(categoriesToInsert) 40 | } 41 | -------------------------------------------------------------------------------- /src/actions/v1/errorTemplate/createErrorTemplate.ts: -------------------------------------------------------------------------------- 1 | import { AppAction } from '@diia-inhouse/diia-app' 2 | 3 | import { ActionVersion, SessionType } from '@diia-inhouse/types' 4 | import { ValidationSchema } from '@diia-inhouse/validators' 5 | 6 | import ErrorTemplateService from '@services/errorTemplate' 7 | 8 | import { ActionResult, CustomActionArguments } from '@interfaces/actions/v1/errorTemplates/createErrorTemplate' 9 | 10 | export default class CreateErrorTemplateAction implements AppAction { 11 | constructor(private errorTemplateService: ErrorTemplateService) {} 12 | 13 | readonly sessionType: SessionType = SessionType.Partner 14 | 15 | readonly actionVersion: ActionVersion = ActionVersion.V1 16 | 17 | readonly name: string = 'createErrorTemplate' 18 | 19 | readonly validationRules: ValidationSchema = { 20 | errorCode: { type: 'number' }, 21 | template: { 22 | type: 'object', 23 | props: { 24 | description: { type: 'string' }, 25 | }, 26 | }, 27 | } 28 | 29 | async handler(args: CustomActionArguments): Promise { 30 | const { params: template } = args 31 | 32 | return await this.errorTemplateService.createErrorTemplate(template) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/middlewares/external.ts: -------------------------------------------------------------------------------- 1 | import { ExternalCommunicator } from '@diia-inhouse/diia-queue' 2 | import { Logger } from '@diia-inhouse/types' 3 | import { utils } from '@diia-inhouse/utils' 4 | 5 | import { jsonResponseTransformer } from '@utils/transformers' 6 | 7 | import { ExternalAlias } from '@interfaces/index' 8 | import { Middleware } from '@interfaces/middlewares' 9 | 10 | export default class ExternalMiddleware { 11 | constructor( 12 | private readonly logger: Logger, 13 | private readonly external: ExternalCommunicator, 14 | ) {} 15 | 16 | addRedirect(externalAlias: ExternalAlias): Middleware { 17 | const { event } = externalAlias 18 | 19 | return async (req, res, next) => { 20 | this.logger.info(`Performing external call event ${event}`) 21 | try { 22 | const externalResponse = await this.external.receiveDirect(event, req.$params) 23 | 24 | const responseTransformer = externalAlias.responseTransformer ?? jsonResponseTransformer 25 | 26 | res.end(responseTransformer(res, externalResponse)) 27 | } catch (err) { 28 | await utils.handleError(err, (apiError) => { 29 | next(apiError) 30 | }) 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/actions/v1/errorTemplate/updateErrorTemplate.ts: -------------------------------------------------------------------------------- 1 | import { AppAction } from '@diia-inhouse/diia-app' 2 | 3 | import { ActionVersion, SessionType } from '@diia-inhouse/types' 4 | import { ValidationSchema } from '@diia-inhouse/validators' 5 | 6 | import ErrorTemplateService from '@services/errorTemplate' 7 | 8 | import { ActionResult, CustomActionArguments } from '@interfaces/actions/v1/errorTemplates/updateErrorTemplate' 9 | 10 | export default class UpdateErrorTemplateAction implements AppAction { 11 | constructor(private readonly errorTemplateService: ErrorTemplateService) {} 12 | 13 | readonly sessionType: SessionType = SessionType.Partner 14 | 15 | readonly actionVersion: ActionVersion = ActionVersion.V1 16 | 17 | readonly name: string = 'updateErrorTemplate' 18 | 19 | readonly validationRules: ValidationSchema = { 20 | errorCode: { type: 'number' }, 21 | template: { 22 | type: 'object', 23 | props: { 24 | description: { type: 'string' }, 25 | }, 26 | }, 27 | } 28 | 29 | async handler(args: CustomActionArguments): Promise { 30 | const { params: template } = args 31 | 32 | return await this.errorTemplateService.updateErrorTemplate(template) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/actions/v1/bumpStoreTags.ts: -------------------------------------------------------------------------------- 1 | import { AppAction } from '@diia-inhouse/diia-app' 2 | 3 | import { StoreTag } from '@diia-inhouse/redis' 4 | import { ActionVersion, SessionType } from '@diia-inhouse/types' 5 | import { ValidationSchema } from '@diia-inhouse/validators' 6 | 7 | import StoreManagementService from '@services/storeManagement' 8 | 9 | import { ActionResult, CustomActionArguments } from '@interfaces/actions/v1/bumpStoreTags' 10 | 11 | export default class BumpStoreTagsAction implements AppAction { 12 | constructor(private readonly storeManagementService: StoreManagementService) {} 13 | 14 | readonly sessionType: SessionType = SessionType.Partner 15 | 16 | readonly actionVersion: ActionVersion = ActionVersion.V1 17 | 18 | readonly name: string = 'bumpStoreTags' 19 | 20 | readonly validationRules: ValidationSchema = { 21 | tags: { 22 | type: 'array', 23 | items: { 24 | type: 'string', 25 | enum: Object.values(StoreTag), 26 | }, 27 | }, 28 | } 29 | 30 | async handler(args: CustomActionArguments): Promise { 31 | const { tags } = args.params 32 | 33 | await this.storeManagementService.bumpTags(tags) 34 | 35 | return { success: true } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/utils/getDeps.ts: -------------------------------------------------------------------------------- 1 | import { DepsFactoryFn, MoleculerService, asClass } from '@diia-inhouse/diia-app' 2 | 3 | import { EventBus, ExternalEventBus, Queue, ScheduledTask, Task } from '@diia-inhouse/diia-queue' 4 | import { CacheService, StoreService } from '@diia-inhouse/redis' 5 | import TestKit, { mockClass } from '@diia-inhouse/test' 6 | 7 | import depsFactory from '@src/deps' 8 | 9 | import { TestDeps } from '@tests/interfaces/utils' 10 | 11 | import { AppDeps } from '@interfaces/application' 12 | import { AppConfig } from '@interfaces/types/config' 13 | 14 | export default async (config: AppConfig): ReturnType> => { 15 | const deps = await depsFactory(config) 16 | 17 | return { 18 | testKit: asClass(TestKit).singleton(), 19 | moleculer: asClass(mockClass(MoleculerService)).singleton(), 20 | queue: asClass(mockClass(Queue)).singleton(), 21 | scheduledTask: asClass(mockClass(ScheduledTask)).singleton(), 22 | store: asClass(mockClass(StoreService)).singleton(), 23 | cache: asClass(mockClass(CacheService)).singleton(), 24 | externalEventBus: asClass(mockClass(ExternalEventBus)).singleton(), 25 | eventBus: asClass(mockClass(EventBus)).singleton(), 26 | task: asClass(mockClass(Task)).singleton(), 27 | ...deps, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /migrations/1-migrate-from-js-to-ts.ts: -------------------------------------------------------------------------------- 1 | import 'module-alias/register' 2 | import { config as migrationConfig } from 'migrate-mongo' 3 | import { Db } from 'mongodb' 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-var-requires 6 | require('dotenv-flow').config({ silent: true }) 7 | 8 | export async function up(db: Db): Promise { 9 | const { changelogCollectionName } = await migrationConfig.read() 10 | 11 | const collectionName = changelogCollectionName 12 | const collections = await db.listCollections().toArray() 13 | 14 | const migrationsExists = collections.find(({ name }) => name === collectionName) 15 | if (!migrationsExists) { 16 | // eslint-disable-next-line no-console 17 | console.log(`Collection ${collectionName} does not exist. Skipping js files conversion.`) 18 | 19 | return 20 | } 21 | 22 | const { matchedCount } = await db.collection(collectionName).updateMany({ fileName: /.js$/ }, [ 23 | { 24 | $set: { 25 | fileName: { 26 | $replaceOne: { input: '$fileName', find: '.js', replacement: '.ts' }, 27 | }, 28 | }, 29 | }, 30 | ]) 31 | 32 | if (matchedCount) { 33 | throw Error(`Past migrations history changed - updated filenames in ${matchedCount} files. Please run migrations again`) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/routes/support.ts: -------------------------------------------------------------------------------- 1 | import { ActionVersion, HttpMethod, SessionType } from '@diia-inhouse/types' 2 | 3 | import { DEFAULT_USER_AUTH, DEFAULT_USER_HEADERS } from './defaults' 4 | 5 | import { AppRoute } from '@interfaces/routes/appRoute' 6 | 7 | const serviceId = 'support' 8 | 9 | const routes: AppRoute[] = [ 10 | { 11 | method: HttpMethod.POST, 12 | path: '/api/:apiVersion/support/crm/gen-deeplink', 13 | proxyTo: { serviceId }, 14 | auth: [{ sessionType: SessionType.PortalUser, version: ActionVersion.V1 }], 15 | headers: [], 16 | }, 17 | 18 | { 19 | method: HttpMethod.GET, 20 | path: '/api/:apiVersion/user/sharing/request/:requestId', 21 | proxyTo: { serviceId }, 22 | auth: DEFAULT_USER_AUTH, 23 | headers: DEFAULT_USER_HEADERS, 24 | }, 25 | { 26 | method: HttpMethod.POST, 27 | path: '/api/:apiVersion/user/sharing/request/:requestId/confirm', 28 | proxyTo: { serviceId }, 29 | auth: DEFAULT_USER_AUTH, 30 | headers: DEFAULT_USER_HEADERS, 31 | }, 32 | { 33 | method: HttpMethod.POST, 34 | path: '/api/:apiVersion/user/sharing/request/:requestId/refuse', 35 | proxyTo: { serviceId }, 36 | auth: DEFAULT_USER_AUTH, 37 | headers: DEFAULT_USER_HEADERS, 38 | }, 39 | ] 40 | 41 | export default routes 42 | -------------------------------------------------------------------------------- /src/services/version.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestError } from '@diia-inhouse/errors' 2 | import { PlatformType } from '@diia-inhouse/types' 3 | 4 | import { MinAppVersionConfigType } from '@interfaces/services/version' 5 | import { AppConfig } from '@interfaces/types/config' 6 | 7 | export default class VersionService { 8 | constructor(private readonly config: AppConfig) {} 9 | 10 | getMinAppVersion( 11 | platformType: PlatformType, 12 | configType: MinAppVersionConfigType = MinAppVersionConfigType.MinAppVersion, 13 | ): string | null { 14 | let minAppVersion: string | null 15 | 16 | switch (platformType) { 17 | case PlatformType.Android: 18 | case PlatformType.Huawei: { 19 | minAppVersion = this.config[configType].android 20 | break 21 | } 22 | case PlatformType.iOS: { 23 | minAppVersion = this.config[configType].ios 24 | break 25 | } 26 | case PlatformType.Browser: { 27 | minAppVersion = null 28 | break 29 | } 30 | default: { 31 | const unknownType: never = platformType 32 | 33 | throw new BadRequestError('Invalid platform type', { type: unknownType }) 34 | } 35 | } 36 | 37 | return minAppVersion 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /processCodesTemplates/invincibility.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "processCode": 54101001, 4 | "template": { 5 | "type": "middleCenterAlignAlert", 6 | "isClosable": false, 7 | "data": { 8 | "icon": "☝️", 9 | "title": "Завантажте мапу", 10 | "description": "Мапа буде доступна на вашому пристрої. Користуйтеся нею будь-коли.\n\nДля завантаження на смартфоні має бути {fileSize} вільної пам’яті.", 11 | "mainButton": { 12 | "name": "Завантажити", 13 | "action": "download" 14 | }, 15 | "alternativeButton": { 16 | "name": "Скасувати", 17 | "action": "skip" 18 | } 19 | } 20 | } 21 | }, 22 | { 23 | "processCode": 54111001, 24 | "template": { 25 | "type": "middleCenterAlignAlert", 26 | "isClosable": false, 27 | "data": { 28 | "icon": "😔", 29 | "title": "Точку не знайдено", 30 | "description": "На жаль, неможливо відобразити детальну інформацію, точку не знайдено.", 31 | "mainButton": { 32 | "name": "Зрозуміло", 33 | "action": "cancel" 34 | } 35 | } 36 | } 37 | } 38 | ] 39 | -------------------------------------------------------------------------------- /src/interfaces/application.ts: -------------------------------------------------------------------------------- 1 | import { formidable } from 'formidable' 2 | import type { ServiceEvents } from 'moleculer' 3 | 4 | import { AppApiService, MoleculerService } from '@diia-inhouse/diia-app' 5 | 6 | import { CmsService } from '@diia-inhouse/cms' 7 | import { HttpDeps } from '@diia-inhouse/http' 8 | 9 | import OpenApiGenerator from '@src/apiDocs/openApiGenerator' 10 | import ApiDocsRoute from '@src/apiDocs/route' 11 | import RoutesBuilder from '@src/routes' 12 | import HeaderValidation from '@src/validation/header' 13 | 14 | import ExternalEventListenersUtils from '@utils/externalEventListeners' 15 | import TrackingUtils from '@utils/tracking' 16 | 17 | import { FaqProvider } from '@interfaces/providers' 18 | import { AppConfig } from '@interfaces/types/config' 19 | 20 | export type InternalDeps = { 21 | trackingUtils: TrackingUtils 22 | externalEventListenersUtils: ExternalEventListenersUtils 23 | routesBuilder: RoutesBuilder 24 | headerValidation: HeaderValidation 25 | apiService: AppApiService 26 | apiDocsRoute: ApiDocsRoute 27 | openApiGenerator: OpenApiGenerator 28 | openApiNodeConnectedEvent: ServiceEvents 29 | faqProvider: FaqProvider 30 | lazyMoleculer: () => MoleculerService 31 | formidable: typeof formidable 32 | } 33 | 34 | export type AppDeps = { 35 | config: AppConfig 36 | cms: CmsService 37 | } & InternalDeps & 38 | HttpDeps 39 | -------------------------------------------------------------------------------- /tests/unit/actions/v1/getAppSettings.spec.ts: -------------------------------------------------------------------------------- 1 | import TestKit, { mockClass } from '@diia-inhouse/test' 2 | 3 | import GetAppSettingsAction from '@actions/v1/getAppSettings' 4 | 5 | import NotificationService from '@services/notification' 6 | import SettingsService from '@services/settings' 7 | import VersionService from '@services/version' 8 | 9 | import { AppSettings } from '@interfaces/services/settings' 10 | 11 | const SettingsServiceMock = mockClass(SettingsService) 12 | 13 | describe(`Action ${GetAppSettingsAction.constructor.name}`, () => { 14 | const testKit = new TestKit() 15 | const settingsService = new SettingsServiceMock({}, {}) 16 | const getAppSettingsAction = new GetAppSettingsAction(settingsService) 17 | 18 | describe('Method `handler`', () => { 19 | it('should successfully return app settings', async () => { 20 | const headers = testKit.session.getHeaders() 21 | const args = { 22 | headers, 23 | } 24 | const expectedAppSettings: AppSettings = { 25 | minVersion: '1.0.0', 26 | needActions: [], 27 | } 28 | 29 | jest.spyOn(settingsService, 'getAppSettings').mockResolvedValueOnce(expectedAppSettings) 30 | 31 | expect(await getAppSettingsAction.handler(args)).toEqual(expectedAppSettings) 32 | }) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /autoMergeRequest.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Extract the host where the server is running, and add the URL to the APIs 3 | [[ $HOST =~ ^https?://[^/]+ ]] && HOST="${BASH_REMATCH[0]}/api/v4/projects/" 4 | 5 | TARGET_BRANCH=master; 6 | # The description of our new MR, we want to remove the branch after the MR has 7 | # been closed 8 | BODY="{ 9 | \"id\": ${CI_PROJECT_ID}, 10 | \"source_branch\": \"${CI_COMMIT_REF_NAME}\", 11 | \"target_branch\": \"${TARGET_BRANCH}\", 12 | \"remove_source_branch\": true, 13 | \"title\": \"WIP: ${CI_COMMIT_REF_NAME}\", 14 | \"assignee_id\":\"${GITLAB_USER_ID}\" 15 | }"; 16 | 17 | # Require a list of all the merge request and take a look if there is already 18 | # one with the same source branch 19 | LISTMR=`curl --silent "${HOST}${CI_PROJECT_ID}/merge_requests?state=opened" --header "PRIVATE-TOKEN:${PRIVATE_TOKEN}"`; 20 | COUNTBRANCHES=`echo ${LISTMR} | grep -o "\"source_branch\":\"${CI_COMMIT_REF_NAME}\"" | wc -l`; 21 | 22 | # No MR found, let's create a new one 23 | if [ ${COUNTBRANCHES} -eq "0" ]; then 24 | curl -X POST "${HOST}${CI_PROJECT_ID}/merge_requests" \ 25 | --header "PRIVATE-TOKEN:${PRIVATE_TOKEN}" \ 26 | --header "Content-Type: application/json" \ 27 | --data "${BODY}"; 28 | 29 | echo "Opened a new merge request: WIP: ${CI_COMMIT_REF_NAME} and assigned to you"; 30 | exit; 31 | fi 32 | 33 | echo "No new merge request opened"; 34 | -------------------------------------------------------------------------------- /tests/unit/actions/v1/getEResidentAppVersion.spec.ts: -------------------------------------------------------------------------------- 1 | import TestKit, { mockClass } from '@diia-inhouse/test' 2 | 3 | import GetEResidentAppVersionAction from '@actions/v1/getEResidentAppVersion' 4 | 5 | import VersionService from '@services/version' 6 | 7 | import { MinAppVersionConfigType } from '@interfaces/services/version' 8 | import { AppConfig } from '@interfaces/types/config' 9 | 10 | const VersionServiceMock = mockClass(VersionService) 11 | 12 | describe(`Action ${GetEResidentAppVersionAction.constructor.name}`, () => { 13 | const testKit = new TestKit() 14 | const versionService = new VersionServiceMock({}) 15 | const getAppVersionAction = new GetEResidentAppVersionAction(versionService) 16 | 17 | describe('Method `handler`', () => { 18 | it('should successfully return min app version', async () => { 19 | const headers = testKit.session.getHeaders() 20 | 21 | const args = { 22 | headers, 23 | } 24 | 25 | jest.spyOn(versionService, 'getMinAppVersion').mockReturnValueOnce('1.0.0') 26 | 27 | expect(await getAppVersionAction.handler(args)).toEqual({ minVersion: '1.0.0' }) 28 | expect(versionService.getMinAppVersion).toHaveBeenCalledWith( 29 | headers.platformType, 30 | MinAppVersionConfigType.MinEResidentAppVersion, 31 | ) 32 | }) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /tests/unit/services/user.spec.ts: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'node:crypto' 2 | 3 | import { MoleculerService } from '@diia-inhouse/diia-app' 4 | 5 | import { ProfileFeature } from '@diia-inhouse/types' 6 | 7 | import UserService from '@services/user' 8 | 9 | import { UserProfileFeatures } from '@interfaces/services/user' 10 | 11 | describe('UserService', () => { 12 | const mockAct = jest.fn() 13 | const lazyMoleculer = (): MoleculerService => ({ 14 | act: mockAct, 15 | }) 16 | 17 | const userService = new UserService(lazyMoleculer) 18 | 19 | beforeEach(() => { 20 | jest.resetAllMocks() 21 | }) 22 | 23 | it('should call getUserProfileFeatures', async () => { 24 | const userIdentifier = randomUUID() 25 | const features = [ProfileFeature.office, ProfileFeature.student] 26 | const expectedResult: UserProfileFeatures = {} 27 | 28 | mockAct.mockResolvedValue(expectedResult) 29 | 30 | const result = await userService.getUserProfileFeatures(userIdentifier, features) 31 | 32 | expect(mockAct).toHaveBeenCalledTimes(1) 33 | expect(mockAct).toHaveBeenCalledWith( 34 | 'User', 35 | { name: 'getUserProfileFeatures', actionVersion: 'v1' }, 36 | { params: { userIdentifier, features } }, 37 | ) 38 | 39 | expect(result).toEqual(expectedResult) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /src/middlewares/multipart.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage } from 'node:http' 2 | 3 | import { formidable as Formidable } from 'formidable' 4 | 5 | import { Logger } from '@diia-inhouse/types' 6 | 7 | import { MimeType, Request, Response, RouteHeaderRawName } from '@interfaces/index' 8 | import { MiddlewareNext } from '@interfaces/middlewares' 9 | 10 | export default class MultipartMiddleware { 11 | constructor( 12 | private readonly logger: Logger, 13 | private readonly formidable: typeof Formidable, 14 | ) {} 15 | 16 | parse(req: Request, _res: Response, next: MiddlewareNext): void { 17 | const requestHeaders = req.headers 18 | const contentType = requestHeaders[RouteHeaderRawName.CONTENT_TYPE] 19 | 20 | if (!contentType || !contentType.includes(MimeType.MultipartMixed)) { 21 | next() 22 | 23 | return 24 | } 25 | 26 | const parser = this.formidable({ multiples: true }) 27 | const incomingRequest = req 28 | 29 | parser.parse(incomingRequest, (err: Error, fields: object): void => { 30 | if (err) { 31 | this.logger.error('Unable to parse multipart', { error: err }) 32 | next(err) 33 | 34 | return 35 | } 36 | 37 | req.$params.body = Object.assign(req.$params.body, { ...fields }) 38 | next() 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /scripts/normalizeProcessCodes.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const { groupBy } = require('lodash') 4 | 5 | function normalizeTemplates(filePath) { 6 | const templatesPath = path.resolve(filePath) 7 | const templates = JSON.parse(fs.readFileSync(templatesPath, { encoding: 'utf8' })) 8 | 9 | const duplicateReplacements = Object.values(groupBy(templates, 'processCode')) 10 | .filter((el) => el.length > 1) 11 | .map((el) => el.at(-1)) 12 | 13 | const replaced = [] 14 | const normalizedTemplates = templates 15 | .map((element) => { 16 | const code = element.processCode 17 | if (replaced.includes(code)) { 18 | return 19 | } 20 | 21 | const replacement = duplicateReplacements.find((repl) => repl.processCode === code) 22 | 23 | if (replacement) { 24 | replaced.push(code) 25 | return replacement 26 | } 27 | 28 | return element 29 | }) 30 | .filter(Boolean) 31 | .sort((a, b) => a.processCode - b.processCode) 32 | 33 | const json = JSON.stringify(normalizedTemplates, null, 4).replace(/\u00A0/g, ' ') + '\n' 34 | fs.writeFileSync(filePath, json) 35 | } 36 | 37 | const dir = 'processCodesTemplates' 38 | const filesList = fs.readdirSync(dir) 39 | 40 | filesList.forEach((fileName) => { 41 | normalizeTemplates([dir, fileName].join('/')) 42 | }) 43 | -------------------------------------------------------------------------------- /tests/unit/actions/v1/bumpStoreTags.spec.ts: -------------------------------------------------------------------------------- 1 | import DiiaLogger from '@diia-inhouse/diia-logger' 2 | import { StoreService, StoreTag } from '@diia-inhouse/redis' 3 | import TestKit, { mockClass } from '@diia-inhouse/test' 4 | 5 | import BumpStoreTagsAction from '@actions/v1/bumpStoreTags' 6 | 7 | import StoreManagementService from '@services/storeManagement' 8 | 9 | const StoreManagementServiceMock = mockClass(StoreManagementService) 10 | 11 | describe(`Action ${BumpStoreTagsAction.constructor.name}`, () => { 12 | const storeManagement = new StoreManagementServiceMock({}, {}) 13 | const bumpStoreTagsAction = new BumpStoreTagsAction(storeManagement) 14 | const testKit = new TestKit() 15 | 16 | describe('Method `handler`', () => { 17 | it('should return success true', async () => { 18 | const headers = testKit.session.getHeaders() 19 | const session = testKit.session.getPartnerSession() 20 | const tags = [{}] 21 | const args = { 22 | params: { 23 | tags, 24 | }, 25 | headers, 26 | session, 27 | } 28 | 29 | jest.spyOn(storeManagement, 'bumpTags').mockResolvedValueOnce() 30 | 31 | expect(await bumpStoreTagsAction.handler(args)).toEqual({ success: true }) 32 | expect(storeManagement.bumpTags).toHaveBeenCalledWith(tags) 33 | }) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /tests/unit/actions/v1/faq/getEResidentFaq.spec.ts: -------------------------------------------------------------------------------- 1 | import { DatabaseService } from '@diia-inhouse/db' 2 | import { StoreService } from '@diia-inhouse/redis' 3 | import { mockClass } from '@diia-inhouse/test' 4 | import { SessionType } from '@diia-inhouse/types' 5 | 6 | import GetEResidentFaqAction from '@actions/v1/faq/getEResidentFaq' 7 | 8 | import FaqService from '@services/faq' 9 | 10 | import { FaqProvider } from '@interfaces/providers' 11 | import { FaqResponse } from '@interfaces/services/faq' 12 | import { AppConfig } from '@interfaces/types/config' 13 | 14 | const FaqServiceMock = mockClass(FaqService) 15 | 16 | describe(`Action ${GetEResidentFaqAction.constructor.name}`, () => { 17 | const faqService = new FaqServiceMock({}, {}, {}, {}) 18 | const getEResidentFaqAction = new GetEResidentFaqAction(faqService) 19 | 20 | describe('Method `handler`', () => { 21 | it('should successfully return faq for EResident', async () => { 22 | const expectedResult: FaqResponse = { 23 | categories: [], 24 | expirationDate: new Date().toISOString(), 25 | } 26 | 27 | jest.spyOn(faqService, 'getFaq').mockResolvedValueOnce(expectedResult) 28 | 29 | expect(await getEResidentFaqAction.handler()).toEqual(expectedResult) 30 | expect(faqService.getFaq).toHaveBeenLastCalledWith(SessionType.EResident, {}) 31 | }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/interfaces/routes/publicService.ts: -------------------------------------------------------------------------------- 1 | export enum PartnerScopeType { 2 | driverLicenseReplacement = 'driverLicenseReplacement', 3 | properUser = 'properUser', 4 | militaryDonation = 'militaryDonation', 5 | idpSupervisor = 'idpSupervisor', 6 | temporaryOccupiedTerritory = 'temporaryOccupiedTerritory', 7 | maintenance = 'maintenance', 8 | mortgage = 'mortgage', 9 | award = 'award', 10 | mia = 'mia', 11 | } 12 | 13 | export enum PartnerDriverLicenseReplacementScope { 14 | StatusCallback = 'statusCallback', 15 | DeliveryCallback = 'deliveryCallback', 16 | Support = 'support', 17 | } 18 | 19 | export enum PartnerMilitaryDonationScope { 20 | UpdateFundReport = 'update-fund-report', 21 | } 22 | 23 | export enum PartnerIdpSupervisorScope { 24 | ResendApplication = 'resend-application', 25 | CheckApplication = 'check-application', 26 | } 27 | 28 | export enum PartnerProperUserScope { 29 | ApplicationStatusCallback = 'applicationStatusCallback', 30 | } 31 | 32 | export enum PartnerTemporaryOccupiedTerritoryScope { 33 | ReplicateFromExternal = 'replicate-from-external', 34 | } 35 | 36 | export enum PartnerMortgageScope { 37 | All = 'all', 38 | } 39 | 40 | export enum PartnerAwardScope { 41 | UploadReceivers = 'upload-receivers', 42 | Revoke = 'revoke', 43 | UpdateDeliveryStatus = 'update-delivery-status', 44 | } 45 | 46 | export enum PartnerMiaScope { 47 | OperationCallback = 'operation-callback', 48 | ResultPushCallback = 'result-push-callback', 49 | } 50 | -------------------------------------------------------------------------------- /migrations/20220523155012-add-error-templates.ts: -------------------------------------------------------------------------------- 1 | import 'module-alias/register' 2 | import { execSync } from 'child_process' 3 | import { resolve } from 'path' 4 | 5 | import { Db } from 'mongodb' 6 | 7 | // eslint-disable-next-line @typescript-eslint/no-var-requires 8 | require('dotenv-flow').config({ silent: true }) 9 | 10 | const collectionName = 'errortemplates' 11 | const filepath: string = resolve('errorTemplates.json') 12 | 13 | export async function up(): Promise { 14 | const mongoImportCmd: string[] = ['mongoimport'] 15 | let mongoUri = 'mongodb://' 16 | if (process.env.MONGO_USER && process.env.MONGO_PASSWORD) { 17 | mongoUri += `${process.env.MONGO_USER}:${process.env.MONGO_PASSWORD}@` 18 | } 19 | 20 | mongoUri += `${process.env.MONGO_HOST}:${process.env.MONGO_PORT}/${process.env.MONGO_DATABASE}` 21 | 22 | const query: string[] = [] 23 | if (process.env.MONGO_AUTH_SOURCE) { 24 | query.push(`authSource=${process.env.MONGO_AUTH_SOURCE}`) 25 | } 26 | 27 | if (process.env.MONGO_REPLICA_SET) { 28 | query.push(`replicaSet=${process.env.MONGO_REPLICA_SET}`) 29 | } 30 | 31 | mongoImportCmd.push(`--uri "${mongoUri}?${query.join('&')}"`) 32 | mongoImportCmd.push(`--collection=${collectionName}`) 33 | mongoImportCmd.push('--jsonArray') 34 | mongoImportCmd.push(`--file=${filepath}`) 35 | 36 | execSync(mongoImportCmd.join(' ')) 37 | } 38 | 39 | export async function down(db: Db): Promise { 40 | await db.dropCollection(collectionName) 41 | } 42 | -------------------------------------------------------------------------------- /errorTemplates.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "errorCode": 1012, 4 | "template": { 5 | "description": "Вказано неіснуючий ідентифікатор шаблону повідомлення" 6 | } 7 | }, 8 | { 9 | "errorCode": 1013, 10 | "template": { 11 | "description": "Помилка валідації" 12 | } 13 | }, 14 | { 15 | "errorCode": 1014, 16 | "template": { 17 | "description": "Невалідний РНОКПП." 18 | } 19 | }, 20 | { 21 | "errorCode": 1015, 22 | "template": { 23 | "description": "Невалідний IBAN." 24 | } 25 | }, 26 | { 27 | "errorCode": 1016, 28 | "template": { 29 | "description": "Термін дії створюваної картки вичерпано. Картку не додано." 30 | } 31 | }, 32 | { 33 | "errorCode": 1017, 34 | "template": { 35 | "description": "Банком вже створено IBAN для вказаного РНОКПП. До одного РНОКПП може бути прив'язано тільки один діючий IBAN з вказаним типом послуги." 36 | } 37 | }, 38 | { 39 | "errorCode": 1018, 40 | "template": { 41 | "description": "Запитуваний до видалення IBAN не знайдено." 42 | } 43 | }, 44 | { 45 | "errorCode": 1019, 46 | "template": { 47 | "description": "Такий IBAN вже існує." 48 | } 49 | }, 50 | { 51 | "errorCode": 1020, 52 | "template": { 53 | "description": "Невалідний номер картки." 54 | } 55 | } 56 | ] 57 | -------------------------------------------------------------------------------- /src/routes/diiaSignature.ts: -------------------------------------------------------------------------------- 1 | import { ActionVersion, HttpMethod, SessionType } from '@diia-inhouse/types' 2 | 3 | import { MimeType, RouteHeaderRawName } from '@interfaces/index' 4 | import { AppRoute } from '@interfaces/routes/appRoute' 5 | 6 | enum DiiaSignatureActions { 7 | SignedDataCallback = 'signedDataCallback', 8 | GetSignedDataReadiness = 'getSignedDataReadiness', 9 | } 10 | 11 | const routes: AppRoute[] = [ 12 | { 13 | method: HttpMethod.POST, 14 | path: '/cabinet/api/:apiVersion/ds/signed-items/callback', 15 | action: DiiaSignatureActions.SignedDataCallback, 16 | auth: [{ sessionType: SessionType.None, version: ActionVersion.V1 }], 17 | headers: [{ name: RouteHeaderRawName.DOCUMENT_REQUEST_TRACE_ID, versions: [ActionVersion.V1] }], 18 | upload: { 19 | allowedMimeTypes: [MimeType.MultipartFormData], 20 | field: 'encodeData', 21 | required: false, 22 | }, 23 | metadata: { 24 | tags: ['Diia Signature'], 25 | summary: 'Diia Signature signed items callback', 26 | }, 27 | }, 28 | { 29 | method: HttpMethod.GET, 30 | path: '/cabinet/api/:apiVersion/ds/signed-items/readiness', 31 | action: DiiaSignatureActions.GetSignedDataReadiness, 32 | auth: [{ sessionType: SessionType.None, version: ActionVersion.V1 }], 33 | metadata: { 34 | tags: ['Auth Methods'], 35 | summary: 'Get Diia Signature Readiness State', 36 | }, 37 | }, 38 | ] 39 | 40 | export default routes 41 | -------------------------------------------------------------------------------- /src/interfaces/queue.ts: -------------------------------------------------------------------------------- 1 | export enum InternalQueueName { 2 | QueueGateway = 'QueueGateway', 3 | } 4 | 5 | export enum ScheduledTaskQueueName { 6 | ScheduledTasksQueueGateway = 'ScheduledTasksQueueGateway', 7 | } 8 | 9 | export enum ScheduledTaskEvent {} 10 | 11 | export enum InternalEvent { 12 | GatewayUserActivity = 'gateway-user-activity', 13 | } 14 | 15 | export enum ExternalEvent { 16 | NotificationTemplateCreate = 'notification.template.create', 17 | NotificationDistributionCreate = 'notification.distribution.create', 18 | NotificationDistributionStatus = 'notification.distribution.status', 19 | NotificationDistributionStatusRecipients = 'notification.distribution.status-recipients', 20 | NotificationDistributionCancel = 'notification.distribution.cancel', 21 | OfficePollCreateAndPublish = 'vote.diia-office.poll.createAndPublish', 22 | OfficePollGet = 'vote.diia-office.poll.get', 23 | OfficePollOnboarding = 'vote.diia-office.poll.onboarding', 24 | OfficePollDelete = 'vote.diia-office.poll.delete', 25 | OfficePollCount = 'vote.diia-office.poll.count', 26 | AcquirerDocumentRequest = 'acquirer.document-request', 27 | AcquirerDocumentResponse = 'acquirer.document-response', 28 | } 29 | 30 | export enum InternalTopic { 31 | TopicGatewayUserActivity = 'TopicGatewayUserActivity', 32 | TopicScheduledTasks = 'TopicScheduledTasks', 33 | } 34 | 35 | export enum ExternalTopic { 36 | NotificationDistribution = 'NotificationDistribution', 37 | OfficePoll = 'OfficePoll', 38 | AcquirerSharing = 'AcquirerSharing', 39 | } 40 | -------------------------------------------------------------------------------- /src/routes/documentDelivery.ts: -------------------------------------------------------------------------------- 1 | import { ActionVersion, HttpMethod, SessionType } from '@diia-inhouse/types' 2 | 3 | import { AppRoute } from '@interfaces/routes/appRoute' 4 | import { PartnerDocumentDeliveryScope, PartnerScopeType } from '@interfaces/routes/documentDelivery' 5 | 6 | enum DocumentDeliveryServiceActions { 7 | DocumentDeliveryStatusCallback = 'documentDeliveryStatusCallback', 8 | CreateDeliveryAndSendOffert = 'createDeliveryAndSendOffert', 9 | } 10 | 11 | const routes: AppRoute[] = [ 12 | { 13 | method: HttpMethod.POST, 14 | path: '/api/:apiVersion/public-service/partner/infotech/delivery-status', 15 | action: DocumentDeliveryServiceActions.DocumentDeliveryStatusCallback, 16 | auth: [ 17 | { 18 | sessionType: SessionType.Partner, 19 | version: ActionVersion.V1, 20 | scopes: { 21 | [PartnerScopeType.documentDelivery]: [PartnerDocumentDeliveryScope.StatusCallback], 22 | }, 23 | }, 24 | ], 25 | }, 26 | { 27 | method: HttpMethod.POST, 28 | path: '/api/:apiVersion/public-service/document-delivery/create', 29 | action: DocumentDeliveryServiceActions.CreateDeliveryAndSendOffert, 30 | auth: [ 31 | { 32 | sessionType: SessionType.Partner, 33 | version: ActionVersion.V1, 34 | scopes: { 35 | [PartnerScopeType.documentDelivery]: [PartnerDocumentDeliveryScope.CreateDelivery], 36 | }, 37 | }, 38 | ], 39 | }, 40 | ] 41 | 42 | export default routes 43 | -------------------------------------------------------------------------------- /src/validation/header.ts: -------------------------------------------------------------------------------- 1 | import { PlatformType } from '@diia-inhouse/types' 2 | 3 | import { AppConfig } from '@interfaces/types/config' 4 | 5 | export default class HeaderValidation { 6 | constructor(private readonly config: AppConfig) {} 7 | 8 | checkMobileUidHeader(mobileUid: string | undefined): boolean { 9 | if (!mobileUid) { 10 | return false 11 | } 12 | 13 | const pattern: RegExp = this.getMobileUuidRegExpPattern() 14 | 15 | return pattern.test(mobileUid) 16 | } 17 | 18 | checkAppVersionHeader(appVersion: string | undefined): boolean { 19 | if (!appVersion) { 20 | return false 21 | } 22 | 23 | const pattern = /^\d{1,2}.\d{1,2}(?:.\d{1,4}){0,2}$/ 24 | 25 | return pattern.test(appVersion) 26 | } 27 | 28 | checkPlatformTypeHeader(platformType: string | undefined): boolean { 29 | if (!platformType) { 30 | return false 31 | } 32 | 33 | return Object.values(PlatformType).includes(platformType) 34 | } 35 | 36 | checkPlatformVersionHeader(platformVersion: string | undefined): boolean { 37 | if (!platformVersion) { 38 | return false 39 | } 40 | 41 | const pattern = /^\d{1,2}(?:\.\d{1,2}){0,2}$/ 42 | 43 | return pattern.test(platformVersion) 44 | } 45 | 46 | private getMobileUuidRegExpPattern(): RegExp { 47 | const uuidVersions: string[] = this.config.auth.deviceHeaderUuidVersions 48 | 49 | return new RegExp(`^[0-9A-F]{8}-[0-9A-F]{4}-[${uuidVersions.join('')}][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$`, 'i') 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/actions/v1/testAcquirerProviderResponse.ts: -------------------------------------------------------------------------------- 1 | import { AppAction } from '@diia-inhouse/diia-app' 2 | 3 | import { ActionVersion, Logger, SessionType } from '@diia-inhouse/types' 4 | import { ValidationSchema } from '@diia-inhouse/validators' 5 | 6 | import MinioStorageService from '@services/minioStorage' 7 | 8 | import { ActionResult, CustomActionArguments } from '@interfaces/actions/v1/testAcquirerProviderResponse' 9 | 10 | export default class TestAcquirerProviderResponseAction implements AppAction { 11 | constructor( 12 | private readonly minioStorageService: MinioStorageService, 13 | 14 | private readonly logger: Logger, 15 | ) {} 16 | 17 | readonly sessionType: SessionType = SessionType.None 18 | 19 | readonly actionVersion: ActionVersion = ActionVersion.V1 20 | 21 | readonly name: string = 'testAcquirerProviderResponse' 22 | 23 | readonly validationRules: ValidationSchema = { 24 | encryptedFile: { type: 'buffer' }, 25 | encryptedFileName: { type: 'string' }, 26 | encodeData: { type: 'string' }, 27 | } 28 | 29 | async handler(args: CustomActionArguments): Promise { 30 | this.logger.debug('Receive encrypted data from DIIA app') 31 | const { encryptedFile, encryptedFileName } = args.params || {} 32 | 33 | // const success: boolean = !!Math.round(Math.random()); 34 | const success = true 35 | let error = '' 36 | if (!success) { 37 | error = 'some error' 38 | } 39 | 40 | await this.minioStorageService.uploadFile(encryptedFile, encryptedFileName) 41 | 42 | return { success, error } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/unit/services/storeManagement.spec.ts: -------------------------------------------------------------------------------- 1 | import { InternalServerError } from '@diia-inhouse/errors' 2 | import { StoreService, StoreTag } from '@diia-inhouse/redis' 3 | 4 | import StoreManagementService from '@services/storeManagement' 5 | 6 | import { logger } from '@mocks/index' 7 | 8 | describe('StoreManagementService', () => { 9 | const mockBumpTags = jest.fn() 10 | const mockStoreService = ({ bumpTags: mockBumpTags }) 11 | const storeManagementService = new StoreManagementService(mockStoreService, logger) 12 | 13 | beforeEach(() => { 14 | jest.clearAllMocks() 15 | }) 16 | 17 | it('should successfully bump tags', async () => { 18 | const tags = [StoreTag.ErrorTemplate, StoreTag.Faq] 19 | 20 | mockBumpTags.mockResolvedValue(true) 21 | 22 | await storeManagementService.bumpTags(tags) 23 | 24 | expect(mockBumpTags).toHaveBeenCalledTimes(1) 25 | expect(mockBumpTags).toHaveBeenCalledWith(tags) 26 | expect(logger.error).not.toHaveBeenCalled() 27 | }) 28 | 29 | it('should throw InternalServerError when bump tags fails', async () => { 30 | const tags = [StoreTag.MilitaryBondsName, StoreTag.PublicService] 31 | 32 | mockBumpTags.mockResolvedValue(false) 33 | 34 | await expect(storeManagementService.bumpTags(tags)).rejects.toThrow(InternalServerError) 35 | 36 | expect(mockBumpTags).toHaveBeenCalledTimes(1) 37 | expect(mockBumpTags).toHaveBeenCalledWith(tags) 38 | expect(logger.error).toHaveBeenCalledTimes(1) 39 | expect(logger.error).toHaveBeenCalledWith('Failed to bump tags', { tags }) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /tests/unit/actions/v1/faq/createFaq.spec.ts: -------------------------------------------------------------------------------- 1 | import { DatabaseService } from '@diia-inhouse/db' 2 | import { StoreService } from '@diia-inhouse/redis' 3 | import TestKit, { mockClass } from '@diia-inhouse/test' 4 | import { SessionType } from '@diia-inhouse/types' 5 | 6 | import CreateFaqAction from '@actions/v1/faq/createFaq' 7 | 8 | import FaqService from '@services/faq' 9 | 10 | import { CustomActionArguments } from '@interfaces/actions/v1/faq/createFaq' 11 | import { FaqProvider } from '@interfaces/providers' 12 | import { AppConfig } from '@interfaces/types/config' 13 | 14 | const FaqServiceMock = mockClass(FaqService) 15 | 16 | describe(`Action ${CreateFaqAction.constructor.name}`, () => { 17 | const testKit = new TestKit() 18 | const faqService = new FaqServiceMock({}, {}, {}, {}) 19 | const createFaqAction = new CreateFaqAction(faqService) 20 | 21 | describe('Method `handler`', () => { 22 | it('should successfully create and return faq', async () => { 23 | const headers = testKit.session.getHeaders() 24 | const args: CustomActionArguments = { 25 | headers, 26 | session: testKit.session.getPartnerSession(), 27 | params: { 28 | categories: [], 29 | session: SessionType.User, 30 | }, 31 | } 32 | 33 | jest.spyOn(faqService, 'createFaq').mockResolvedValueOnce(args.params) 34 | 35 | expect(await createFaqAction.handler(args)).toEqual(args.params) 36 | expect(faqService.createFaq).toHaveBeenLastCalledWith(args.params) 37 | }) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /tests/unit/actions/v1/faq/updateFaq.spec.ts: -------------------------------------------------------------------------------- 1 | import { DatabaseService } from '@diia-inhouse/db' 2 | import { StoreService } from '@diia-inhouse/redis' 3 | import TestKit, { mockClass } from '@diia-inhouse/test' 4 | import { SessionType } from '@diia-inhouse/types' 5 | 6 | import UpdateFaqAction from '@actions/v1/faq/updateFaq' 7 | 8 | import FaqService from '@services/faq' 9 | 10 | import { CustomActionArguments } from '@interfaces/actions/v1/faq/updateFaq' 11 | import { FaqProvider } from '@interfaces/providers' 12 | import { AppConfig } from '@interfaces/types/config' 13 | 14 | const FaqServiceMock = mockClass(FaqService) 15 | 16 | describe(`Action ${UpdateFaqAction.constructor.name}`, () => { 17 | const testKit = new TestKit() 18 | const faqService = new FaqServiceMock({}, {}, {}, {}) 19 | const updateFaqAction = new UpdateFaqAction(faqService) 20 | 21 | describe('Method `handler`', () => { 22 | it('should successfully update and return faq', async () => { 23 | const headers = testKit.session.getHeaders() 24 | const args: CustomActionArguments = { 25 | headers, 26 | session: testKit.session.getPartnerSession(), 27 | params: { 28 | categories: [], 29 | session: SessionType.User, 30 | }, 31 | } 32 | 33 | jest.spyOn(faqService, 'replaceFaq').mockResolvedValueOnce(args.params) 34 | 35 | expect(await updateFaqAction.handler(args)).toEqual(args.params) 36 | expect(faqService.replaceFaq).toHaveBeenLastCalledWith(args.params) 37 | }) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /src/providers/faq/strapiFaqProvider.ts: -------------------------------------------------------------------------------- 1 | import { CmsCollectionType, CmsService } from '@diia-inhouse/cms' 2 | import { SessionType, UserFeatures } from '@diia-inhouse/types' 3 | import { profileFeaturesToList } from '@diia-inhouse/utils' 4 | 5 | import FaqCategoryCmsDataMapper from '@dataMappers/cms/faqCategoryCmsDataMapper' 6 | 7 | import { CmsFaqCategory } from '@interfaces/models/cms/faq' 8 | import { FaqCategory } from '@interfaces/models/faqCategory' 9 | import { FaqProvider } from '@interfaces/providers' 10 | 11 | export default class StrapiFaqProvider implements FaqProvider { 12 | constructor( 13 | private readonly cms: CmsService, 14 | private readonly faqCategoryCmsDataMapper: FaqCategoryCmsDataMapper, 15 | ) {} 16 | 17 | async getCategoriesList(session: SessionType, userFeatures: UserFeatures): Promise { 18 | const featuresList = profileFeaturesToList(userFeatures) 19 | const list = await this.cms.getList( 20 | CmsCollectionType.FaqCategory, 21 | { 22 | populate: 'deep', 23 | filters: { 24 | $and: [ 25 | { sessionType: session }, 26 | { 27 | $or: [ 28 | ...featuresList.map((feature) => ({ features: { value: feature } })), 29 | { features: { value: { $null: true } } }, 30 | ], 31 | }, 32 | ], 33 | }, 34 | }, 35 | this.faqCategoryCmsDataMapper, 36 | ) 37 | 38 | return list.data 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/unit/actions/v1/getPublicServices.spec.ts: -------------------------------------------------------------------------------- 1 | import DiiaLogger from '@diia-inhouse/diia-logger' 2 | import { CacheService } from '@diia-inhouse/redis' 3 | import { mockClass } from '@diia-inhouse/test' 4 | 5 | import PublicServiceDataMapper from '@src/dataMappers/publicServiceDataMapper' 6 | 7 | import GetPublicServicesAction from '@actions/v1/getPublicServices' 8 | 9 | import PublicServicesListService from '@services/publicServicesList' 10 | 11 | import { PublicServiceResponse, PublicServiceStatus } from '@interfaces/services/publicServicesList' 12 | 13 | const PublicServicesListServiceMock = mockClass(PublicServicesListService) 14 | 15 | describe(`Action ${GetPublicServicesAction.constructor.name}`, () => { 16 | const publicServicesListService = new PublicServicesListServiceMock({}, {}, {}) 17 | const getPublicServicesAction = new GetPublicServicesAction(publicServicesListService) 18 | 19 | describe('Method `handler`', () => { 20 | it('should successfully return list of public services', async () => { 21 | const expectedResult: PublicServiceResponse[] = [ 22 | { 23 | code: 'criminal-record-certificate', 24 | name: 'criminalRecordCertificate', 25 | status: PublicServiceStatus.Active, 26 | }, 27 | ] 28 | 29 | jest.spyOn(publicServicesListService, 'getPublicServices').mockResolvedValueOnce(expectedResult) 30 | 31 | expect(await getPublicServicesAction.handler()).toEqual({ publicServices: expectedResult }) 32 | expect(publicServicesListService.getPublicServices).toHaveBeenCalledWith() 33 | }) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /src/apiDocs/openApiNodeConnectedEvent.ts: -------------------------------------------------------------------------------- 1 | import { kebabCase } from 'lodash' 2 | import type { BrokerNode, Context, Service, ServiceEvents } from 'moleculer' 3 | 4 | import OpenApiGenerator from '@src/apiDocs/openApiGenerator' 5 | import RoutesBuilder from '@src/routes' 6 | 7 | import { AppConfig } from '@interfaces/types/config' 8 | 9 | export default (config: Partial, routesBuilder: RoutesBuilder, openApiGenerator: OpenApiGenerator): ServiceEvents => ({ 10 | '$node.connected': { 11 | handler(ctx: Context<{ node: BrokerNode; reconnected: boolean }>): void { 12 | if (!config?.swagger?.isEnabled) { 13 | return 14 | } 15 | 16 | const { node, reconnected } = ctx.params 17 | if (!node?.services?.length || reconnected) { 18 | return 19 | } 20 | 21 | const { services: brokerServices } = node 22 | const services = (brokerServices.filter((service) => service.name !== '$node')) 23 | const { servicesRoutes } = routesBuilder 24 | 25 | for (const service of services) { 26 | const serviceName: string = service.name 27 | const serviceNameKebabCase: string = kebabCase(serviceName) 28 | const serviceSpec = openApiGenerator.specs[serviceNameKebabCase] 29 | const routesDeclarations = servicesRoutes[serviceName] 30 | 31 | if (routesDeclarations && serviceSpec) { 32 | const spec = openApiGenerator.generateOpenApiSchema(serviceName, routesDeclarations, service) 33 | 34 | openApiGenerator.specs[serviceNameKebabCase] = spec 35 | } 36 | } 37 | }, 38 | }, 39 | }) 40 | -------------------------------------------------------------------------------- /src/models/faqCategory.ts: -------------------------------------------------------------------------------- 1 | import { Model, Schema, model, models } from '@diia-inhouse/db' 2 | import { ProfileFeature, SessionType } from '@diia-inhouse/types' 3 | 4 | import { FaqCategory, FaqItem, FaqParameter, FaqParameterType } from '@interfaces/models/faqCategory' 5 | 6 | const faqParameterSchema = new Schema( 7 | { 8 | type: { type: String, enum: Object.values(FaqParameterType), required: true }, 9 | data: { 10 | name: { type: String, required: true }, 11 | alt: { type: String, required: true }, 12 | resource: { type: String, required: true }, 13 | }, 14 | }, 15 | { 16 | _id: false, 17 | }, 18 | ) 19 | 20 | const faqItemSchema = new Schema( 21 | { 22 | question: { type: String, required: true }, 23 | answer: { type: String, required: true }, 24 | parameters: { type: [faqParameterSchema] }, 25 | subFeatures: { type: [String], default: [] }, 26 | }, 27 | { 28 | _id: false, 29 | }, 30 | ) 31 | 32 | export const faqCategorySchema = new Schema( 33 | { 34 | code: { type: String, required: true }, 35 | name: { type: String, required: true }, 36 | faq: { type: [faqItemSchema], required: true }, 37 | features: { type: [String], enum: Object.values(ProfileFeature), default: [] }, 38 | sessionType: { type: String, enum: Object.values(SessionType), required: true }, 39 | order: { type: Number, default: 1000 }, 40 | }, 41 | { 42 | _id: false, 43 | }, 44 | ) 45 | 46 | faqCategorySchema.index({ code: 1, sessionType: 1 }, { unique: true }) 47 | 48 | export default >models.FaqCategory || model('FaqCategory', faqCategorySchema) 49 | -------------------------------------------------------------------------------- /processCodesTemplates/address.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "processCode": 57101001, 4 | "template": { 5 | "type": "middleCenterAlignAlert", 6 | "isClosable": false, 7 | "data": { 8 | "icon": "😔", 9 | "title": "Доставка неможлива", 10 | "description": "Йой! Немає доставки у ваш населений пункт. Будь ласка, виберіть інший.", 11 | "mainButton": { 12 | "name": "Зрозуміло", 13 | "action": "refill" 14 | } 15 | } 16 | } 17 | }, 18 | { 19 | "processCode": 57101002, 20 | "template": { 21 | "type": "middleCenterAlignAlert", 22 | "isClosable": false, 23 | "data": { 24 | "icon": "😔", 25 | "title": "Послуга недоступна", 26 | "description": "Сервіс Укрпошти захворів, його вже лікують 🍃. Спробуйте, будь ласка, пізніше.", 27 | "mainButton": { 28 | "name": "Зрозуміло", 29 | "action": "publicServices" 30 | } 31 | } 32 | } 33 | }, 34 | { 35 | "processCode": 57101003, 36 | "template": { 37 | "type": "middleCenterAlignAlert", 38 | "isClosable": false, 39 | "data": { 40 | "icon": "🥺", 41 | "title": "Адресу не визначено", 42 | "description": "Йой! Дії не вдалося встановити зв’язок з реєстрами та визначити адресу. Повертайтеся трохи пізніше.", 43 | "mainButton": { 44 | "name": "Зрозуміло", 45 | "action": "publicServices" 46 | } 47 | } 48 | } 49 | } 50 | ] 51 | -------------------------------------------------------------------------------- /tests/unit/actions/v1/faq/getFaq.spec.ts: -------------------------------------------------------------------------------- 1 | const utilsMock = { 2 | extractProfileFeatures: jest.fn(), 3 | } 4 | 5 | jest.mock('@diia-inhouse/utils', () => utilsMock) 6 | 7 | import { DatabaseService } from '@diia-inhouse/db' 8 | import { StoreService } from '@diia-inhouse/redis' 9 | import TestKit, { mockClass } from '@diia-inhouse/test' 10 | import { UserActionArguments } from '@diia-inhouse/types' 11 | 12 | import GetFaqAction from '@actions/v1/faq/getFaq' 13 | 14 | import FaqService from '@services/faq' 15 | 16 | import { FaqProvider } from '@interfaces/providers' 17 | import { FaqResponse } from '@interfaces/services/faq' 18 | import { AppConfig } from '@interfaces/types/config' 19 | 20 | const FaqServiceMock = mockClass(FaqService) 21 | 22 | describe(`Action ${GetFaqAction.constructor.name}`, () => { 23 | const testKit = new TestKit() 24 | const faqService = new FaqServiceMock({}, {}, {}, {}) 25 | const getFaqAction = new GetFaqAction(faqService) 26 | 27 | describe('Method `handler`', () => { 28 | it('should successfully get faq', async () => { 29 | const headers = testKit.session.getHeaders() 30 | const args: UserActionArguments = { 31 | headers, 32 | session: testKit.session.getUserSession(), 33 | } 34 | const expectedResult: FaqResponse = { 35 | categories: [], 36 | expirationDate: new Date().toISOString(), 37 | } 38 | 39 | jest.spyOn(faqService, 'getFaq').mockResolvedValueOnce(expectedResult) 40 | 41 | expect(await getFaqAction.handler(args)).toEqual(expectedResult) 42 | expect(faqService.getFaq).toHaveBeenLastCalledWith(args.session.sessionType, {}) 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /src/routes/documents/passports.ts: -------------------------------------------------------------------------------- 1 | import { ActionVersion, HttpMethod, SessionType } from '@diia-inhouse/types' 2 | 3 | import { RouteHeaderRawName } from '@interfaces/index' 4 | import { AppRoute } from '@interfaces/routes/appRoute' 5 | 6 | enum DocumentsActions { 7 | GetPassports = 'getPassports', 8 | GetRegistrationPlaceForPassport = 'getRegistrationPlaceForPassport', 9 | } 10 | 11 | const routes: AppRoute[] = [ 12 | { 13 | method: HttpMethod.GET, 14 | path: '/api/:apiVersion/documents/passport', 15 | action: DocumentsActions.GetPassports, 16 | auth: [{ sessionType: SessionType.User, version: ActionVersion.V1 }], 17 | headers: [ 18 | { name: RouteHeaderRawName.MOBILE_UID, versions: [ActionVersion.V1] }, 19 | { name: RouteHeaderRawName.APP_VERSION, versions: [ActionVersion.V1] }, 20 | { name: RouteHeaderRawName.PLATFORM_TYPE, versions: [ActionVersion.V1] }, 21 | { name: RouteHeaderRawName.PLATFORM_VERSION, versions: [ActionVersion.V1] }, 22 | ], 23 | metadata: { 24 | tags: ['Get Documents From Registry'], 25 | }, 26 | }, 27 | { 28 | method: HttpMethod.POST, 29 | path: '/api/:apiVersion/documents/registration-place/passport', 30 | action: DocumentsActions.GetRegistrationPlaceForPassport, 31 | auth: [{ sessionType: SessionType.User, version: ActionVersion.V1 }], 32 | headers: [ 33 | { name: RouteHeaderRawName.MOBILE_UID, versions: [ActionVersion.V1] }, 34 | { name: RouteHeaderRawName.APP_VERSION, versions: [ActionVersion.V1] }, 35 | { name: RouteHeaderRawName.PLATFORM_TYPE, versions: [ActionVersion.V1] }, 36 | { name: RouteHeaderRawName.PLATFORM_VERSION, versions: [ActionVersion.V1] }, 37 | ], 38 | }, 39 | ] 40 | 41 | export default routes 42 | -------------------------------------------------------------------------------- /tests/unit/dataMappers/publicServiceDataMapper.spec.ts: -------------------------------------------------------------------------------- 1 | import BankDataMapper from '@dataMappers/publicServiceDataMapper' 2 | 3 | import { PublicServiceStatus } from '@interfaces/services/publicServicesList' 4 | 5 | describe(`Mapper ${BankDataMapper.name}`, () => { 6 | let bankDataMapper: BankDataMapper 7 | 8 | beforeEach(() => { 9 | bankDataMapper = new BankDataMapper() 10 | }) 11 | 12 | it('should map PublicService to PublicServiceResponse correctly', () => { 13 | const publicService = { 14 | id: 1, 15 | name: 'Bank A', 16 | code: 'BANK_A', 17 | status: PublicServiceStatus.Active, 18 | sortOrder: 5, 19 | } 20 | 21 | const expectedResponse = { 22 | id: 1, 23 | name: 'Bank A', 24 | code: 'BANK_A', 25 | status: PublicServiceStatus.Active, 26 | } 27 | 28 | const result = bankDataMapper.toEntity(publicService) 29 | 30 | expect(result).toEqual(expectedResponse) 31 | }) 32 | 33 | it('should not modify the original PublicService object', () => { 34 | const publicServiceTemplate = { 35 | id: 1, 36 | name: 'Bank A', 37 | code: 'BANK_A', 38 | status: PublicServiceStatus.Active, 39 | sortOrder: 5, 40 | } 41 | 42 | const expectedResponse = { 43 | id: 1, 44 | name: 'Bank A', 45 | code: 'BANK_A', 46 | status: PublicServiceStatus.Active, 47 | } 48 | const originPublicServiceTemplateCopy = { ...publicServiceTemplate } 49 | 50 | const result = bankDataMapper.toEntity(publicServiceTemplate) 51 | 52 | expect(result).toEqual(expectedResponse) 53 | expect(publicServiceTemplate).toEqual(originPublicServiceTemplateCopy) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /tests/unit/services/notification.spec.ts: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'node:crypto' 2 | 3 | import { MoleculerService } from '@diia-inhouse/diia-app' 4 | 5 | import { ActionSession } from '@diia-inhouse/types' 6 | 7 | import NotificationService from '@src/services/notification' 8 | 9 | describe('NotificationService', () => { 10 | const mockAct = jest.fn() 11 | const lazyMoleculer = (): MoleculerService => ({ 12 | act: mockAct, 13 | }) 14 | const notificationService = new NotificationService(lazyMoleculer) 15 | 16 | beforeEach(() => { 17 | jest.clearAllMocks() 18 | }) 19 | 20 | it('should make a request', async () => { 21 | const actionName = 'testAction' 22 | const params = { param1: 'value1', param2: 'value2' } 23 | const session: ActionSession = ({ userIdentifier: randomUUID(), deviceToken: 'device-123' }) 24 | const expectedResult = { result: 'test' } 25 | 26 | mockAct.mockResolvedValue(expectedResult) 27 | 28 | const result = await notificationService.makeRequest(actionName, params, session) 29 | 30 | expect(mockAct).toHaveBeenCalledTimes(1) 31 | expect(mockAct).toHaveBeenCalledWith('Notification', { name: actionName, actionVersion: 'v1' }, { params, session }) 32 | 33 | expect(result).toEqual(expectedResult) 34 | }) 35 | 36 | it('should check if push token exists', async () => { 37 | const expectedResult = { hasPushToken: true } 38 | 39 | mockAct.mockResolvedValue(expectedResult) 40 | 41 | const result = await notificationService.hasPushToken() 42 | 43 | expect(mockAct).toHaveBeenCalledTimes(1) 44 | expect(mockAct).toHaveBeenCalledWith('Notification', { name: 'hasPushToken', actionVersion: 'v1' }) 45 | 46 | expect(result).toEqual(expectedResult) 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the Diia project 2 | 3 | We're pleased that you're interested in contributing to the Diia project. At the moment we're welcoming contributions in various forms and we want to make contributing as easy and transparent as possible. You're welcome to contribute in any of the following ways: 4 | 5 | - Reporting a bug 6 | - Discussing the current state of the code 7 | - Proposing new features or ideas 8 | 9 | In the future we'll be considering welcoming code contributions and expanding our contributor community. 10 | 11 | ## Report using Issues 12 | 13 | We use GitHub issues to track public bugs. Report a bug, feature, idea or open a discussion point by [opening a new issue](../../issues/new); it's that easy! 14 | 15 | For bugs related to vulnerabilities or security concerns please feel free to contact us directly at [modt.opensource@thedigital.gov.ua](mailto:modt.opensource@thedigital.gov.ua). 16 | 17 | We'd also request that you detail bug reports with detail, background and sample code. Typically a great bug report includes: 18 | 19 | - A quick summary and/or background 20 | - Steps to reproduce 21 | - Be specific and provide sample code if you can. 22 | - What you expected would happen 23 | - What actually happens 24 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 25 | 26 | For ideas, suggestions and discussion you're free to use any format that you find suitable. 27 | 28 | ## Licensing 29 | 30 | By contributing, you agree that your contributions will be licensed under the EUPL. 31 | 32 | You may obtain a copy of the License at [https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12](https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12). 33 | 34 | Questions regarding the Diia project, the License and any re-use should be directed to [modt.opensource@thedigital.gov.ua](mailto:modt.opensource@thedigital.gov.ua). 35 | -------------------------------------------------------------------------------- /src/utils/tracking.ts: -------------------------------------------------------------------------------- 1 | import { Span, SpanStatusCode } from '@diia-inhouse/diia-app' 2 | 3 | import { MetricsService, RequestStatus, TotalRequestsLabelsMap } from '@diia-inhouse/diia-metrics' 4 | import { ErrorType } from '@diia-inhouse/errors' 5 | import { HttpStatusCode } from '@diia-inhouse/types' 6 | 7 | import { ResponseError } from '@interfaces/index' 8 | 9 | export default class TrackingUtils { 10 | constructor(private readonly metrics: MetricsService) {} 11 | 12 | trackError(error: ResponseError, labels: Partial, requestStart?: bigint, span?: Span): void { 13 | if (!span?.isRecording()) { 14 | return 15 | } 16 | 17 | const { name, message, code = HttpStatusCode.INTERNAL_SERVER_ERROR, type = ErrorType.Unoperated } = error 18 | 19 | span?.recordException({ message, code, name }) 20 | 21 | if (requestStart) { 22 | this.metrics.totalTimerMetric.observeSeconds( 23 | { 24 | ...labels, 25 | status: RequestStatus.Failed, 26 | errorType: type, 27 | statusCode: code, 28 | }, 29 | process.hrtime.bigint() - requestStart, 30 | ) 31 | } 32 | 33 | span?.setStatus({ code: SpanStatusCode.ERROR, message }) 34 | 35 | span?.end() 36 | } 37 | 38 | trackSuccess(labels: Partial, requestStart?: bigint, span?: Span): void { 39 | if (!span?.isRecording()) { 40 | return 41 | } 42 | 43 | if (requestStart) { 44 | this.metrics.totalTimerMetric.observeSeconds( 45 | { 46 | ...labels, 47 | status: RequestStatus.Successful, 48 | }, 49 | process.hrtime.bigint() - requestStart, 50 | ) 51 | } 52 | 53 | span?.end() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/validationRules/faqCategory.ts: -------------------------------------------------------------------------------- 1 | import { ProfileFeature } from '@diia-inhouse/types' 2 | import { ValidationRule } from '@diia-inhouse/validators' 3 | 4 | import { FaqParameterType } from '@interfaces/models/faqCategory' 5 | 6 | export const getFaqCategoryRule = (optional = false): ValidationRule => { 7 | return { 8 | type: 'object', 9 | props: { 10 | code: { type: 'string', optional }, 11 | name: { type: 'string', optional }, 12 | features: { 13 | type: 'array', 14 | optional: true, 15 | items: { type: 'string', enum: Object.values(ProfileFeature) }, 16 | }, 17 | faq: { 18 | type: 'array', 19 | optional, 20 | items: { 21 | type: 'object', 22 | props: { 23 | question: { type: 'string' }, 24 | answer: { type: 'string' }, 25 | parameters: { 26 | type: 'array', 27 | items: { 28 | type: 'object', 29 | props: { 30 | type: { type: 'string', enum: Object.values(FaqParameterType) }, 31 | data: { 32 | type: 'object', 33 | props: { 34 | name: { type: 'string' }, 35 | alt: { type: 'string' }, 36 | resource: { type: 'string' }, 37 | }, 38 | }, 39 | }, 40 | }, 41 | }, 42 | }, 43 | }, 44 | }, 45 | }, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/unit/actions/v1/errorTemplate/updateErrorTemplate.spec.ts: -------------------------------------------------------------------------------- 1 | import { StoreService } from '@diia-inhouse/redis' 2 | import TestKit, { mockClass } from '@diia-inhouse/test' 3 | 4 | import UpdateErrorTemplateAction from '@actions/v1/errorTemplate/updateErrorTemplate' 5 | 6 | import ErrorTemplateService from '@services/errorTemplate' 7 | 8 | import ErrorTemplateDataMapper from '@dataMappers/errorTemplateDataMapper' 9 | 10 | import { CustomActionArguments } from '@interfaces/actions/v1/errorTemplates/updateErrorTemplate' 11 | import { ErrorTemplateResult } from '@interfaces/services/errorTemplate' 12 | 13 | const ErrorTemplateServiceMock = mockClass(ErrorTemplateService) 14 | 15 | describe(`Action ${UpdateErrorTemplateAction.constructor.name}`, () => { 16 | const testKit = new TestKit() 17 | const errorTemplateService = new ErrorTemplateServiceMock({}, {}) 18 | const updateErrorTemplateAction = new UpdateErrorTemplateAction(errorTemplateService) 19 | 20 | describe('Method `handler`', () => { 21 | it('should successfully update and return error template', async () => { 22 | const headers = testKit.session.getHeaders() 23 | const template: ErrorTemplateResult = { 24 | id: 'template-id', 25 | errorCode: 1111, 26 | template: { 27 | description: 'description', 28 | }, 29 | } 30 | 31 | const args: CustomActionArguments = { 32 | headers, 33 | session: testKit.session.getPartnerSession(), 34 | params: template, 35 | } 36 | 37 | jest.spyOn(errorTemplateService, 'updateErrorTemplate').mockResolvedValueOnce(template) 38 | 39 | expect(await updateErrorTemplateAction.handler(args)).toEqual(template) 40 | expect(errorTemplateService.updateErrorTemplate).toHaveBeenLastCalledWith(template) 41 | }) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /migrations/20220505111351-add-faq-for-user.ts: -------------------------------------------------------------------------------- 1 | import 'module-alias/register' 2 | import { execSync } from 'child_process' 3 | import { resolve } from 'path' 4 | 5 | import { Db } from 'mongodb' 6 | 7 | import { SessionType } from '@diia-inhouse/types' 8 | 9 | // eslint-disable-next-line @typescript-eslint/no-var-requires 10 | require('dotenv-flow').config({ silent: true }) 11 | 12 | const collectionName = 'faqs' 13 | const filepath: string = resolve('faq.json') 14 | const sessions: SessionType[] = [SessionType.User, SessionType.EResident] 15 | 16 | export async function up(db: Db): Promise { 17 | try { 18 | await db.dropCollection(collectionName) 19 | } catch (err) {} 20 | 21 | await db.createCollection(collectionName) 22 | 23 | const mongoImportCmd: string[] = ['mongoimport'] 24 | let mongoUri = 'mongodb://' 25 | if (process.env.MONGO_USER && process.env.MONGO_PASSWORD) { 26 | mongoUri += `${process.env.MONGO_USER}:${process.env.MONGO_PASSWORD}@` 27 | } 28 | 29 | mongoUri += `${process.env.MONGO_HOST}:${process.env.MONGO_PORT}/${process.env.MONGO_DATABASE}` 30 | 31 | const query: string[] = [] 32 | if (process.env.MONGO_AUTH_SOURCE) { 33 | query.push(`authSource=${process.env.MONGO_AUTH_SOURCE}`) 34 | } 35 | 36 | if (process.env.MONGO_REPLICA_SET) { 37 | query.push(`replicaSet=${process.env.MONGO_REPLICA_SET}`) 38 | } 39 | 40 | mongoImportCmd.push(`--uri "${mongoUri}?${query.join('&')}"`) 41 | mongoImportCmd.push(`--collection=${collectionName}`) 42 | mongoImportCmd.push(`--file=${filepath}`) 43 | 44 | const tasks = sessions.map(async (session: SessionType) => { 45 | execSync(mongoImportCmd.join(' ')) 46 | await db.collection(collectionName).updateOne({ session: { $exists: false } }, { $set: { session } }) 47 | }) 48 | 49 | await Promise.all(tasks) 50 | } 51 | 52 | export async function down(db: Db): Promise { 53 | await db.dropCollection(collectionName) 54 | } 55 | -------------------------------------------------------------------------------- /tests/unit/actions/v1/testAcquirerProviderResponse.spec.ts: -------------------------------------------------------------------------------- 1 | import DiiaLogger from '@diia-inhouse/diia-logger' 2 | import TestKit, { mockClass } from '@diia-inhouse/test' 3 | 4 | import TestAcquirerProviderResponseAction from '@actions/v1/testAcquirerProviderResponse' 5 | 6 | import MinioStorageService from '@services/minioStorage' 7 | 8 | import { CustomActionArguments } from '@interfaces/actions/v1/testAcquirerProviderResponse' 9 | import { AppConfig } from '@interfaces/types/config' 10 | 11 | const DiiaLoggerMock = mockClass(DiiaLogger) 12 | const MinioStorageServiceMock = mockClass(MinioStorageService) 13 | 14 | describe(`Action ${TestAcquirerProviderResponseAction.constructor.name}`, () => { 15 | const testKit = new TestKit() 16 | const logger = new DiiaLoggerMock() 17 | const minioStorageService = new MinioStorageServiceMock({}, logger) 18 | const testAcquirerProviderResponseAction = new TestAcquirerProviderResponseAction(minioStorageService, logger) 19 | 20 | describe('Method `handler`', () => { 21 | it('should return success true and empty error', async () => { 22 | const headers = testKit.session.getHeaders() 23 | const params = { 24 | encodeData: 'encoded-data', 25 | encryptedFile: Buffer.from('encrypted-file'), 26 | encryptedFileName: 'document.pdf', 27 | } 28 | const args: CustomActionArguments = { 29 | params, 30 | headers, 31 | ...params, 32 | } 33 | 34 | jest.spyOn(minioStorageService, 'uploadFile').mockResolvedValueOnce() 35 | 36 | expect(await testAcquirerProviderResponseAction.handler(args)).toEqual({ success: true, error: '' }) 37 | expect(minioStorageService.uploadFile).toHaveBeenCalledWith(params.encryptedFile, params.encryptedFileName) 38 | expect(logger.debug).toHaveBeenLastCalledWith('Receive encrypted data from DIIA app') 39 | }) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /tests/unit/services/version.spec.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestError } from '@diia-inhouse/errors' 2 | import { PlatformType } from '@diia-inhouse/types' 3 | 4 | import VersionService from '@services/version' 5 | 6 | import { MinAppVersionConfigType } from '@interfaces/services/version' 7 | import { AppConfig } from '@interfaces/types/config' 8 | 9 | describe('VersionService', () => { 10 | const versionService = new VersionService({ 11 | [MinAppVersionConfigType.MinAppVersion]: { 12 | android: '1.0.0', 13 | ios: '1.1.0', 14 | }, 15 | [MinAppVersionConfigType.MinEResidentAppVersion]: { 16 | android: '1.3.0', 17 | ios: '1.2.0', 18 | }, 19 | }) 20 | 21 | describe('method: `getMinAppVersion`', () => { 22 | it.each([ 23 | ['1.0.0', PlatformType.Android, MinAppVersionConfigType.MinAppVersion], 24 | ['1.0.0', PlatformType.Huawei, MinAppVersionConfigType.MinAppVersion], 25 | ['1.1.0', PlatformType.iOS, MinAppVersionConfigType.MinAppVersion], 26 | [null, PlatformType.Browser, MinAppVersionConfigType.MinAppVersion], 27 | ['1.3.0', PlatformType.Android, MinAppVersionConfigType.MinEResidentAppVersion], 28 | ['1.3.0', PlatformType.Huawei, MinAppVersionConfigType.MinEResidentAppVersion], 29 | ['1.2.0', PlatformType.iOS, MinAppVersionConfigType.MinEResidentAppVersion], 30 | [null, PlatformType.Browser, MinAppVersionConfigType.MinEResidentAppVersion], 31 | ])('should return min version %s for %s platform type and %s config type', (expectedMinVersion, platformType, configType) => { 32 | expect(versionService.getMinAppVersion(platformType, configType)).toEqual(expectedMinVersion) 33 | }) 34 | it('should throw error for unknown platform type', () => { 35 | expect(() => { 36 | versionService.getMinAppVersion('unknown') 37 | }).toThrow(new BadRequestError('Invalid platform type', { type: 'unknown' })) 38 | }) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /src/routes/documents/taxpayerCard.ts: -------------------------------------------------------------------------------- 1 | import { ActionVersion, HttpMethod, SessionType } from '@diia-inhouse/types' 2 | 3 | import { RouteHeaderRawName } from '@interfaces/index' 4 | import { AppRoute } from '@interfaces/routes/appRoute' 5 | 6 | enum DocumentsActions { 7 | ShareTaxpayerCard = 'shareTaxpayerCard', 8 | VerifyTaxpayerCard = 'verifyTaxpayerCard', 9 | } 10 | 11 | const routes: AppRoute[] = [ 12 | { 13 | method: HttpMethod.GET, 14 | path: '/api/:apiVersion/documents/taxpayer-card/:documentId/share', 15 | action: DocumentsActions.ShareTaxpayerCard, 16 | auth: [{ sessionType: SessionType.User, version: ActionVersion.V1 }], 17 | headers: [ 18 | { name: RouteHeaderRawName.MOBILE_UID, versions: [ActionVersion.V1] }, 19 | { name: RouteHeaderRawName.TOKEN, versions: [ActionVersion.V1] }, 20 | { name: RouteHeaderRawName.APP_VERSION, versions: [ActionVersion.V1] }, 21 | { name: RouteHeaderRawName.PLATFORM_TYPE, versions: [ActionVersion.V1] }, 22 | { name: RouteHeaderRawName.PLATFORM_VERSION, versions: [ActionVersion.V1] }, 23 | ], 24 | metadata: { 25 | tags: ['Share Document'], 26 | }, 27 | }, 28 | { 29 | method: HttpMethod.GET, 30 | path: '/api/:apiVersion/documents/taxpayer-card/verify', 31 | action: DocumentsActions.VerifyTaxpayerCard, 32 | auth: [{ sessionType: SessionType.User, version: ActionVersion.V1, blockAccess: false }], 33 | headers: [ 34 | { name: RouteHeaderRawName.MOBILE_UID, versions: [ActionVersion.V1] }, 35 | { name: RouteHeaderRawName.TOKEN, versions: [ActionVersion.V1] }, 36 | { name: RouteHeaderRawName.APP_VERSION, versions: [ActionVersion.V1] }, 37 | { name: RouteHeaderRawName.PLATFORM_TYPE, versions: [ActionVersion.V1] }, 38 | { name: RouteHeaderRawName.PLATFORM_VERSION, versions: [ActionVersion.V1] }, 39 | ], 40 | metadata: { 41 | tags: ['Verify Document'], 42 | }, 43 | }, 44 | ] 45 | 46 | export default routes 47 | -------------------------------------------------------------------------------- /src/routes/documents/foreignPassport.ts: -------------------------------------------------------------------------------- 1 | import { ActionVersion, HttpMethod, SessionType } from '@diia-inhouse/types' 2 | 3 | import { RouteHeaderRawName } from '@interfaces/index' 4 | import { AppRoute } from '@interfaces/routes/appRoute' 5 | 6 | enum DocumentsActions { 7 | ShareForeignPassport = 'shareForeignPassport', 8 | VerifyForeignPassport = 'verifyForeignPassport', 9 | } 10 | 11 | const routes: AppRoute[] = [ 12 | { 13 | method: HttpMethod.GET, 14 | path: '/api/:apiVersion/documents/foreign-passport/:documentId/share', 15 | action: DocumentsActions.ShareForeignPassport, 16 | auth: [{ sessionType: SessionType.User, version: ActionVersion.V1 }], 17 | headers: [ 18 | { name: RouteHeaderRawName.MOBILE_UID, versions: [ActionVersion.V1] }, 19 | { name: RouteHeaderRawName.TOKEN, versions: [ActionVersion.V1] }, 20 | { name: RouteHeaderRawName.APP_VERSION, versions: [ActionVersion.V1] }, 21 | { name: RouteHeaderRawName.PLATFORM_TYPE, versions: [ActionVersion.V1] }, 22 | { name: RouteHeaderRawName.PLATFORM_VERSION, versions: [ActionVersion.V1] }, 23 | ], 24 | metadata: { 25 | tags: ['Share Document'], 26 | }, 27 | }, 28 | { 29 | method: HttpMethod.GET, 30 | path: '/api/:apiVersion/documents/foreign-passport/verify', 31 | action: DocumentsActions.VerifyForeignPassport, 32 | auth: [{ sessionType: SessionType.User, version: ActionVersion.V1, blockAccess: false }], 33 | headers: [ 34 | { name: RouteHeaderRawName.MOBILE_UID, versions: [ActionVersion.V1] }, 35 | { name: RouteHeaderRawName.TOKEN, versions: [ActionVersion.V1] }, 36 | { name: RouteHeaderRawName.APP_VERSION, versions: [ActionVersion.V1] }, 37 | { name: RouteHeaderRawName.PLATFORM_TYPE, versions: [ActionVersion.V1] }, 38 | { name: RouteHeaderRawName.PLATFORM_VERSION, versions: [ActionVersion.V1] }, 39 | ], 40 | metadata: { 41 | tags: ['Verify Document'], 42 | }, 43 | }, 44 | ] 45 | 46 | export default routes 47 | -------------------------------------------------------------------------------- /src/routes/documents/internalPassport.ts: -------------------------------------------------------------------------------- 1 | import { ActionVersion, HttpMethod, SessionType } from '@diia-inhouse/types' 2 | 3 | import { RouteHeaderRawName } from '@interfaces/index' 4 | import { AppRoute } from '@interfaces/routes/appRoute' 5 | 6 | enum DocumentsActions { 7 | ShareInternalPassport = 'shareInternalPassport', 8 | VerifyInternalPassport = 'verifyInternalPassport', 9 | } 10 | 11 | const routes: AppRoute[] = [ 12 | { 13 | method: HttpMethod.GET, 14 | path: '/api/:apiVersion/documents/internal-passport/:documentId/share', 15 | action: DocumentsActions.ShareInternalPassport, 16 | auth: [{ sessionType: SessionType.User, version: ActionVersion.V1 }], 17 | headers: [ 18 | { name: RouteHeaderRawName.MOBILE_UID, versions: [ActionVersion.V1] }, 19 | { name: RouteHeaderRawName.TOKEN, versions: [ActionVersion.V1] }, 20 | { name: RouteHeaderRawName.APP_VERSION, versions: [ActionVersion.V1] }, 21 | { name: RouteHeaderRawName.PLATFORM_TYPE, versions: [ActionVersion.V1] }, 22 | { name: RouteHeaderRawName.PLATFORM_VERSION, versions: [ActionVersion.V1] }, 23 | ], 24 | metadata: { 25 | tags: ['Share Document'], 26 | }, 27 | }, 28 | { 29 | method: HttpMethod.GET, 30 | path: '/api/:apiVersion/documents/internal-passport/verify', 31 | action: DocumentsActions.VerifyInternalPassport, 32 | auth: [{ sessionType: SessionType.User, version: ActionVersion.V1, blockAccess: false }], 33 | headers: [ 34 | { name: RouteHeaderRawName.MOBILE_UID, versions: [ActionVersion.V1] }, 35 | { name: RouteHeaderRawName.TOKEN, versions: [ActionVersion.V1] }, 36 | { name: RouteHeaderRawName.APP_VERSION, versions: [ActionVersion.V1] }, 37 | { name: RouteHeaderRawName.PLATFORM_TYPE, versions: [ActionVersion.V1] }, 38 | { name: RouteHeaderRawName.PLATFORM_VERSION, versions: [ActionVersion.V1] }, 39 | ], 40 | metadata: { 41 | tags: ['Verify Document'], 42 | }, 43 | }, 44 | ] 45 | 46 | export default routes 47 | -------------------------------------------------------------------------------- /tests/unit/actions/v1/errorTemplate/getErrorTemplateByErrorCode.spec.ts: -------------------------------------------------------------------------------- 1 | import { StoreService } from '@diia-inhouse/redis' 2 | import TestKit, { mockClass } from '@diia-inhouse/test' 3 | 4 | import GetErrorTemplateByErrorCodeAction from '@actions/v1/errorTemplate/getErrorTemplateByErrorCode' 5 | 6 | import ErrorTemplateService from '@services/errorTemplate' 7 | 8 | import ErrorTemplateDataMapper from '@dataMappers/errorTemplateDataMapper' 9 | 10 | import { CustomActionArguments } from '@interfaces/actions/v1/errorTemplates/getErrorTemplateByErrorCode' 11 | import { ErrorTemplateResult } from '@interfaces/services/errorTemplate' 12 | 13 | const ErrorTemplateServiceMock = mockClass(ErrorTemplateService) 14 | 15 | describe(`Action ${GetErrorTemplateByErrorCodeAction.constructor.name}`, () => { 16 | const testKit = new TestKit() 17 | const errorTemplateService = new ErrorTemplateServiceMock({}, {}) 18 | const getErrorTemplateByErrorCodeAction = new GetErrorTemplateByErrorCodeAction(errorTemplateService) 19 | 20 | describe('Method `handler`', () => { 21 | it('should successfully return error template by code', async () => { 22 | const headers = testKit.session.getHeaders() 23 | const expectedResult: ErrorTemplateResult = { 24 | id: 'template-id', 25 | errorCode: 1111, 26 | template: { 27 | description: 'description', 28 | }, 29 | } 30 | 31 | const args: CustomActionArguments = { 32 | headers, 33 | session: testKit.session.getPartnerSession(), 34 | params: { 35 | errorCode: expectedResult.errorCode, 36 | }, 37 | } 38 | 39 | jest.spyOn(errorTemplateService, 'fetchErrorTemplateByCode').mockResolvedValueOnce(expectedResult) 40 | 41 | expect(await getErrorTemplateByErrorCodeAction.handler(args)).toEqual(expectedResult) 42 | expect(errorTemplateService.fetchErrorTemplateByCode).toHaveBeenLastCalledWith(expectedResult.errorCode) 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /tests/unit/actions/v1/errorTemplate/createErrorTemplate.spec.ts: -------------------------------------------------------------------------------- 1 | import { StoreService } from '@diia-inhouse/redis' 2 | import TestKit, { mockClass } from '@diia-inhouse/test' 3 | 4 | import CreateErrorTemplateAction from '@actions/v1/errorTemplate/createErrorTemplate' 5 | 6 | import ErrorTemplateService from '@services/errorTemplate' 7 | 8 | import ErrorTemplateDataMapper from '@dataMappers/errorTemplateDataMapper' 9 | 10 | import { CustomActionArguments } from '@interfaces/actions/v1/errorTemplates/createErrorTemplate' 11 | import { ErrorTemplate } from '@interfaces/models/errorTemplate' 12 | import { ErrorTemplateResult } from '@interfaces/services/errorTemplate' 13 | 14 | const ErrorTemplateServiceMock = mockClass(ErrorTemplateService) 15 | 16 | describe(`Action ${CreateErrorTemplateAction.constructor.name}`, () => { 17 | const testKit = new TestKit() 18 | const errorTemplateService = new ErrorTemplateServiceMock({}, {}) 19 | const createErrorTemplateAction = new CreateErrorTemplateAction(errorTemplateService) 20 | 21 | describe('Method `handler`', () => { 22 | it('should successfully create error template and return it', async () => { 23 | const headers = testKit.session.getHeaders() 24 | const template: ErrorTemplate = { 25 | errorCode: 1111, 26 | template: { 27 | description: 'description', 28 | }, 29 | } 30 | const args: CustomActionArguments = { 31 | headers, 32 | session: testKit.session.getPartnerSession(), 33 | params: template, 34 | } 35 | 36 | const expectedResult: ErrorTemplateResult = { 37 | id: 'template-id', 38 | ...template, 39 | } 40 | 41 | jest.spyOn(errorTemplateService, 'createErrorTemplate').mockResolvedValueOnce(expectedResult) 42 | 43 | expect(await createErrorTemplateAction.handler(args)).toEqual(expectedResult) 44 | expect(errorTemplateService.createErrorTemplate).toHaveBeenLastCalledWith(template) 45 | }) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /src/services/settings.ts: -------------------------------------------------------------------------------- 1 | import { compare as compareSemver } from 'compare-versions' 2 | 3 | import { AppUserActionHeaders } from '@diia-inhouse/types' 4 | 5 | import NotificationService from '@services/notification' 6 | import VersionService from '@services/version' 7 | 8 | import { AppAction, AppSettings } from '@interfaces/services/settings' 9 | 10 | export default class SettingsService { 11 | constructor( 12 | private readonly notificationService: NotificationService, 13 | private readonly versionService: VersionService, 14 | ) {} 15 | 16 | async getAppSettings(headers: AppUserActionHeaders): Promise { 17 | const { platformType } = headers 18 | 19 | const needActions: AppAction[] = [] 20 | const tasks = Object.values(AppAction).map(async (action) => { 21 | const isActionNeeded = await this.isActionNeeded(action, headers) 22 | 23 | if (isActionNeeded) { 24 | needActions.push(action) 25 | } 26 | }) 27 | 28 | await Promise.all(tasks) 29 | 30 | return { 31 | needActions, 32 | minVersion: this.versionService.getMinAppVersion(platformType), 33 | } 34 | } 35 | 36 | async isActionNeeded(action: AppAction, appHeaders: AppUserActionHeaders): Promise { 37 | switch (action) { 38 | case AppAction.PushTokenUpdate: { 39 | const { hasPushToken } = await this.notificationService.hasPushToken() 40 | 41 | return !hasPushToken 42 | } 43 | case AppAction.ForceUpdate: { 44 | const { platformType, appVersion } = appHeaders 45 | 46 | const minVersion = this.versionService.getMinAppVersion(platformType) 47 | 48 | if (!minVersion) { 49 | return false 50 | } 51 | 52 | return compareSemver(minVersion, appVersion, '>') 53 | } 54 | default: { 55 | const unhandledAction: never = action 56 | 57 | throw new TypeError(`Unhandled app action: ${unhandledAction}`) 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/interfaces/profileFeature/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-use-before-define */ 2 | 3 | import { ProfileFeature } from '@diia-inhouse/types' 4 | 5 | type ProfileFeatureChecker = (feature: ProfileFeature) => boolean 6 | 7 | export abstract class ProfileFeatureExpression { 8 | protected constructor(readonly features: ProfileFeature[]) {} 9 | 10 | abstract match(checker: ProfileFeatureChecker): boolean 11 | 12 | static only(feature: ProfileFeature): ProfileFeatureExpression { 13 | return new ProfileFeatureOnly(feature) 14 | } 15 | 16 | static oneOf(features: ProfileFeature[]): ProfileFeatureExpression { 17 | return new ProfileFeatureOneOf(features) 18 | } 19 | 20 | static allOf(features: ProfileFeature[]): ProfileFeatureExpression { 21 | return new ProfileFeatureAll(features) 22 | } 23 | 24 | static mayHave(features: ProfileFeature[]): ProfileFeatureExpression { 25 | return new ProfileFeatureMayHave(features) 26 | } 27 | } 28 | 29 | class ProfileFeatureOnly extends ProfileFeatureExpression { 30 | constructor(feature: ProfileFeature) { 31 | super([feature]) 32 | } 33 | 34 | match(checker: ProfileFeatureChecker): boolean { 35 | return checker(this.features[0]) 36 | } 37 | } 38 | 39 | class ProfileFeatureOneOf extends ProfileFeatureExpression { 40 | match(checker: ProfileFeatureChecker): boolean { 41 | for (const feature of this.features) { 42 | if (checker(feature)) { 43 | return true 44 | } 45 | } 46 | 47 | return false 48 | } 49 | } 50 | 51 | class ProfileFeatureAll extends ProfileFeatureExpression { 52 | match(checker: ProfileFeatureChecker): boolean { 53 | for (const feature of this.features) { 54 | if (!checker(feature)) { 55 | return false 56 | } 57 | } 58 | 59 | return true 60 | } 61 | } 62 | 63 | class ProfileFeatureMayHave extends ProfileFeatureExpression { 64 | match(checker: ProfileFeatureChecker): boolean { 65 | for (const feature of this.features) { 66 | checker(feature) 67 | } 68 | 69 | return true 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/unit/validation/header.spec.ts: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'node:crypto' 2 | 3 | import { PlatformType } from '@diia-inhouse/types' 4 | 5 | import HeaderValidation from '@src/validation/header' 6 | 7 | import { AppConfig } from '@interfaces/types/config' 8 | 9 | describe('HeaderValidation', () => { 10 | const config = { auth: { deviceHeaderUuidVersions: ['4', '5'] } } 11 | const headerValidation = new HeaderValidation(config) 12 | 13 | describe('method: `checkMobileUidHeader`', () => { 14 | it.each([ 15 | [false, ''], 16 | [false, 'invalid-uuid'], 17 | [true, randomUUID()], 18 | [true, '1cbbade9-666a-5556-a3b2-97c012213abb'], 19 | ])('should return %s in case mobile uuid %s', (expectedResult, inputMobileUuid) => { 20 | expect(headerValidation.checkMobileUidHeader(inputMobileUuid)).toEqual(expectedResult) 21 | }) 22 | }) 23 | 24 | describe('method: `checkAppVersionHeader`', () => { 25 | it.each([ 26 | [false, ''], 27 | [false, 'invalid-version'], 28 | [true, '1.0.0'], 29 | ])('should return %s in case app version %s', (expectedResult, inputVersion) => { 30 | expect(headerValidation.checkAppVersionHeader(inputVersion)).toEqual(expectedResult) 31 | }) 32 | }) 33 | 34 | describe('method: `checkPlatformTypeHeader`', () => { 35 | it.each([ 36 | [false, ''], 37 | [false, 'invalid-platform-type'], 38 | [true, PlatformType.Android], 39 | ])('should return %s in case platform type %s', (expectedResult, inputPlatformType) => { 40 | expect(headerValidation.checkPlatformTypeHeader(inputPlatformType)).toEqual(expectedResult) 41 | }) 42 | }) 43 | 44 | describe('method: `checkPlatformVersionHeader`', () => { 45 | it.each([ 46 | [false, ''], 47 | [false, 'invalid-platform-version'], 48 | [true, '13'], 49 | ])('should return %s in case platform type %s', (expectedResult, inputPlatformVersion) => { 50 | expect(headerValidation.checkPlatformVersionHeader(inputPlatformVersion)).toEqual(expectedResult) 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /processCodesTemplates/backOfficePetition.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "processCode": 18101001, 4 | "template": { 5 | "type": "middleCenterAlignAlert", 6 | "isClosable": false, 7 | "data": { 8 | "icon": "👍", 9 | "title": "Петицію підтримано!", 10 | "description": "Слідкуйте за сповіщеннями в застосунку або на порталі Дія, щоб дізнатись результати петиції.", 11 | "mainButton": { 12 | "name": "Зрозуміло", 13 | "action": "ok" 14 | } 15 | } 16 | } 17 | }, 18 | { 19 | "processCode": 18101002, 20 | "template": { 21 | "type": "middleCenterAlignAlert", 22 | "isClosable": false, 23 | "data": { 24 | "icon": "👍", 25 | "title": "Петицію підтримано!", 26 | "description": "Слідкуйте за сповіщеннями в застосунку або на порталі Дія, щоб дізнатись результати петиції.", 27 | "mainButton": { 28 | "name": "Зрозуміло", 29 | "action": "refresh" 30 | } 31 | } 32 | } 33 | }, 34 | { 35 | "processCode": 18101004, 36 | "template": { 37 | "type": "smallAlert", 38 | "isClosable": false, 39 | "data": { 40 | "icon": "😞", 41 | "title": "Данної петиції не існує", 42 | "mainButton": { 43 | "name": "Зрозуміло", 44 | "action": "petitions" 45 | } 46 | } 47 | } 48 | }, 49 | { 50 | "processCode": 18111001, 51 | "template": { 52 | "type": "middleCenterAlignAlert", 53 | "isClosable": false, 54 | "data": { 55 | "icon": "👍", 56 | "title": "Петицію подано!", 57 | "description": "Слідкуйте за сповіщеннями в застосунку або на порталі Дія, щоб дізнатись результати петиції.", 58 | "mainButton": { 59 | "name": "Зрозуміло", 60 | "action": "petition" 61 | } 62 | } 63 | } 64 | } 65 | ] 66 | -------------------------------------------------------------------------------- /src/externalEventListeners/notification/notificationTemplateCreate.ts: -------------------------------------------------------------------------------- 1 | import { EventBusListener, ExternalEventBus } from '@diia-inhouse/diia-queue' 2 | import { ValidationError } from '@diia-inhouse/errors' 3 | import { ValidationSchema } from '@diia-inhouse/validators' 4 | 5 | import NotificationService from '@services/notification' 6 | 7 | import ExternalEventListenersUtils from '@utils/externalEventListeners' 8 | 9 | import { MessagePayload } from '@interfaces/externalEventListeners' 10 | import { EventPayload } from '@interfaces/externalEventListeners/notification' 11 | import { ExternalEvent } from '@interfaces/queue' 12 | 13 | export default class NotificationTemplateCreateEventListener implements EventBusListener { 14 | constructor( 15 | private readonly externalEventListenersUtils: ExternalEventListenersUtils, 16 | private readonly notificationService: NotificationService, 17 | 18 | private readonly externalEventBus: ExternalEventBus, 19 | ) {} 20 | 21 | readonly event: ExternalEvent = ExternalEvent.NotificationTemplateCreate 22 | 23 | readonly validationRules: ValidationSchema = { 24 | uuid: { type: 'string' }, 25 | request: { 26 | type: 'object', 27 | props: { 28 | partnerId: { type: 'string' }, 29 | }, 30 | }, 31 | } 32 | 33 | async validationErrorHandler(error: ValidationError, uuid: string): Promise { 34 | await this.externalEventListenersUtils.handleError(error, this.event, uuid) 35 | } 36 | 37 | async handler(payload: EventPayload): Promise { 38 | const { uuid, request } = payload 39 | const { partnerId, ...params } = request 40 | const event: ExternalEvent = this.event 41 | try { 42 | const [route, session] = await this.externalEventListenersUtils.validatePartnerRoute(event, partnerId) 43 | 44 | const response: unknown = await this.notificationService.makeRequest(route.action, params, session) 45 | const message: MessagePayload = { uuid, response } 46 | 47 | await this.externalEventBus.publish(event, message) 48 | } catch (err) { 49 | await this.externalEventListenersUtils.handleError(err, event, uuid) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/externalEventListeners/notification/notificationDistributionCancel.ts: -------------------------------------------------------------------------------- 1 | import { EventBusListener, ExternalEventBus } from '@diia-inhouse/diia-queue' 2 | import { ValidationError } from '@diia-inhouse/errors' 3 | import { ValidationSchema } from '@diia-inhouse/validators' 4 | 5 | import NotificationService from '@services/notification' 6 | 7 | import ExternalEventListenersUtils from '@utils/externalEventListeners' 8 | 9 | import { MessagePayload } from '@interfaces/externalEventListeners' 10 | import { EventPayload } from '@interfaces/externalEventListeners/notification' 11 | import { ExternalEvent } from '@interfaces/queue' 12 | 13 | export default class NotificationDistributionCancelEventListener implements EventBusListener { 14 | constructor( 15 | private readonly externalEventListenersUtils: ExternalEventListenersUtils, 16 | private readonly notificationService: NotificationService, 17 | 18 | private readonly externalEventBus: ExternalEventBus, 19 | ) {} 20 | 21 | readonly event: ExternalEvent = ExternalEvent.NotificationDistributionCancel 22 | 23 | readonly validationRules: ValidationSchema = { 24 | uuid: { type: 'string' }, 25 | request: { 26 | type: 'object', 27 | props: { 28 | partnerId: { type: 'string' }, 29 | }, 30 | }, 31 | } 32 | 33 | async validationErrorHandler(error: ValidationError, uuid: string): Promise { 34 | await this.externalEventListenersUtils.handleError(error, this.event, uuid) 35 | } 36 | 37 | async handler(payload: EventPayload): Promise { 38 | const { uuid, request } = payload 39 | const { partnerId, ...params } = request 40 | const event: ExternalEvent = this.event 41 | try { 42 | const [route, session] = await this.externalEventListenersUtils.validatePartnerRoute(event, partnerId) 43 | 44 | const response: unknown = await this.notificationService.makeRequest(route.action, params, session) 45 | const message: MessagePayload = { uuid, response } 46 | 47 | await this.externalEventBus.publish(event, message) 48 | } catch (err) { 49 | await this.externalEventListenersUtils.handleError(err, event, uuid) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/externalEventListeners/notification/notificationDistributionCreate.ts: -------------------------------------------------------------------------------- 1 | import { EventBusListener, ExternalEventBus } from '@diia-inhouse/diia-queue' 2 | import { ValidationError } from '@diia-inhouse/errors' 3 | import { ValidationSchema } from '@diia-inhouse/validators' 4 | 5 | import NotificationService from '@services/notification' 6 | 7 | import ExternalEventListenersUtils from '@utils/externalEventListeners' 8 | 9 | import { MessagePayload } from '@interfaces/externalEventListeners' 10 | import { EventPayload } from '@interfaces/externalEventListeners/notification' 11 | import { ExternalEvent } from '@interfaces/queue' 12 | 13 | export default class NotificationDistributionCreateEventListener implements EventBusListener { 14 | constructor( 15 | private readonly externalEventListenersUtils: ExternalEventListenersUtils, 16 | private readonly notificationService: NotificationService, 17 | 18 | private readonly externalEventBus: ExternalEventBus, 19 | ) {} 20 | 21 | readonly event: ExternalEvent = ExternalEvent.NotificationDistributionCreate 22 | 23 | readonly validationRules: ValidationSchema = { 24 | uuid: { type: 'string' }, 25 | request: { 26 | type: 'object', 27 | props: { 28 | partnerId: { type: 'string' }, 29 | }, 30 | }, 31 | } 32 | 33 | async validationErrorHandler(error: ValidationError, uuid: string): Promise { 34 | await this.externalEventListenersUtils.handleError(error, this.event, uuid) 35 | } 36 | 37 | async handler(payload: EventPayload): Promise { 38 | const { uuid, request } = payload 39 | const { partnerId, ...params } = request 40 | const event: ExternalEvent = this.event 41 | try { 42 | const [route, session] = await this.externalEventListenersUtils.validatePartnerRoute(event, partnerId) 43 | 44 | const response: unknown = await this.notificationService.makeRequest(route.action, params, session) 45 | const message: MessagePayload = { uuid, response } 46 | 47 | await this.externalEventBus.publish(event, message) 48 | } catch (err) { 49 | await this.externalEventListenersUtils.handleError(err, event, uuid) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/externalEventListeners/notification/notificationDistributionStatus.ts: -------------------------------------------------------------------------------- 1 | import { EventBusListener, ExternalEventBus } from '@diia-inhouse/diia-queue' 2 | import { ValidationError } from '@diia-inhouse/errors' 3 | import { ValidationSchema } from '@diia-inhouse/validators' 4 | 5 | import NotificationService from '@services/notification' 6 | 7 | import ExternalEventListenersUtils from '@utils/externalEventListeners' 8 | 9 | import { MessagePayload } from '@interfaces/externalEventListeners' 10 | import { EventPayload } from '@interfaces/externalEventListeners/notification' 11 | import { ExternalEvent } from '@interfaces/queue' 12 | 13 | export default class NotificationDistributionStatusEventListener implements EventBusListener { 14 | constructor( 15 | private readonly externalEventListenersUtils: ExternalEventListenersUtils, 16 | private readonly notificationService: NotificationService, 17 | 18 | private readonly externalEventBus: ExternalEventBus, 19 | ) {} 20 | 21 | readonly event: ExternalEvent = ExternalEvent.NotificationDistributionStatus 22 | 23 | readonly validationRules: ValidationSchema = { 24 | uuid: { type: 'string' }, 25 | request: { 26 | type: 'object', 27 | props: { 28 | partnerId: { type: 'string' }, 29 | }, 30 | }, 31 | } 32 | 33 | async validationErrorHandler(error: ValidationError, uuid: string): Promise { 34 | await this.externalEventListenersUtils.handleError(error, this.event, uuid) 35 | } 36 | 37 | async handler(payload: EventPayload): Promise { 38 | const { uuid, request } = payload 39 | const { partnerId, ...params } = request 40 | const event: ExternalEvent = this.event 41 | try { 42 | const [route, session] = await this.externalEventListenersUtils.validatePartnerRoute(event, partnerId) 43 | 44 | const response: unknown = await this.notificationService.makeRequest(route.action, params, session) 45 | const message: MessagePayload = { uuid, response } 46 | 47 | await this.externalEventBus.publish(event, message) 48 | } catch (err) { 49 | await this.externalEventListenersUtils.handleError(err, event, uuid) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/externalEventListeners/notification/notificationDistributionStatusRecipients.ts: -------------------------------------------------------------------------------- 1 | import { EventBusListener, ExternalEventBus } from '@diia-inhouse/diia-queue' 2 | import { ValidationError } from '@diia-inhouse/errors' 3 | import { ValidationSchema } from '@diia-inhouse/validators' 4 | 5 | import NotificationService from '@services/notification' 6 | 7 | import ExternalEventListenersUtils from '@utils/externalEventListeners' 8 | 9 | import { MessagePayload } from '@interfaces/externalEventListeners' 10 | import { EventPayload } from '@interfaces/externalEventListeners/notification' 11 | import { ExternalEvent } from '@interfaces/queue' 12 | 13 | export default class NotificationDistributionStatusRecipientsEventListener implements EventBusListener { 14 | constructor( 15 | private readonly externalEventListenersUtils: ExternalEventListenersUtils, 16 | private readonly notificationService: NotificationService, 17 | 18 | private readonly externalEventBus: ExternalEventBus, 19 | ) {} 20 | 21 | readonly event: ExternalEvent = ExternalEvent.NotificationDistributionStatusRecipients 22 | 23 | readonly validationRules: ValidationSchema = { 24 | uuid: { type: 'string' }, 25 | request: { 26 | type: 'object', 27 | props: { 28 | partnerId: { type: 'string' }, 29 | }, 30 | }, 31 | } 32 | 33 | async validationErrorHandler(error: ValidationError, uuid: string): Promise { 34 | await this.externalEventListenersUtils.handleError(error, this.event, uuid) 35 | } 36 | 37 | async handler(payload: EventPayload): Promise { 38 | const { uuid, request } = payload 39 | const { partnerId, ...params } = request 40 | const event: ExternalEvent = this.event 41 | try { 42 | const [route, session] = await this.externalEventListenersUtils.validatePartnerRoute(event, partnerId) 43 | 44 | const response: unknown = await this.notificationService.makeRequest(route.action, params, session) 45 | const message: MessagePayload = { uuid, response } 46 | 47 | await this.externalEventBus.publish(event, message) 48 | } catch (err) { 49 | await this.externalEventListenersUtils.handleError(err, event, uuid) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/unit/actions/v1/errorTemplate/getErrorTemplatesList.spec.ts: -------------------------------------------------------------------------------- 1 | import { StoreService } from '@diia-inhouse/redis' 2 | import TestKit, { mockClass } from '@diia-inhouse/test' 3 | 4 | import GetErrorTemplatesListAction from '@actions/v1/errorTemplate/getErrorTemplatesList' 5 | 6 | import ErrorTemplateService from '@services/errorTemplate' 7 | 8 | import ErrorTemplateDataMapper from '@dataMappers/errorTemplateDataMapper' 9 | 10 | import { CustomActionArguments } from '@interfaces/actions/v1/errorTemplates/getErrorTemplatesList' 11 | import { ErrorTemplateListResult } from '@interfaces/services/errorTemplate' 12 | 13 | const ErrorTemplateServiceMock = mockClass(ErrorTemplateService) 14 | 15 | describe(`Action ${GetErrorTemplatesListAction.constructor.name}`, () => { 16 | const testKit = new TestKit() 17 | const errorTemplateService = new ErrorTemplateServiceMock({}, {}) 18 | const getErrorTemplatesListAction = new GetErrorTemplatesListAction(errorTemplateService) 19 | 20 | describe('Method `handler`', () => { 21 | it('should successfully return error templates list', async () => { 22 | const headers = testKit.session.getHeaders() 23 | const expectedResult: ErrorTemplateListResult = { 24 | errorTemplates: [ 25 | { 26 | id: 'template-id', 27 | errorCode: 1111, 28 | template: { 29 | description: 'description', 30 | }, 31 | }, 32 | ], 33 | total: 1, 34 | } 35 | 36 | const args: CustomActionArguments = { 37 | headers, 38 | session: testKit.session.getPartnerSession(), 39 | params: { 40 | limit: 10, 41 | skip: 0, 42 | }, 43 | } 44 | 45 | jest.spyOn(errorTemplateService, 'getErrorTemplatesList').mockResolvedValueOnce(expectedResult) 46 | 47 | expect(await getErrorTemplatesListAction.handler(args)).toEqual(expectedResult) 48 | expect(errorTemplateService.getErrorTemplatesList).toHaveBeenLastCalledWith({ 49 | skip: args.params.skip, 50 | limit: args.params.limit, 51 | }) 52 | }) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /src/actions/v1/testAcquirerProviderResponses.ts: -------------------------------------------------------------------------------- 1 | import { AppAction } from '@diia-inhouse/diia-app' 2 | 3 | import { ActionVersion, Logger, SessionType } from '@diia-inhouse/types' 4 | import { ValidationSchema } from '@diia-inhouse/validators' 5 | 6 | import MinioStorageService from '@services/minioStorage' 7 | 8 | import { ActionResult, CustomActionArguments } from '@interfaces/actions/v1/testAcquirerProviderResponse' 9 | import { File } from '@interfaces/index' 10 | import { DocumentType } from '@interfaces/services/documents' 11 | 12 | export default class TestAcquirerProviderShareAppAppAction implements AppAction { 13 | constructor( 14 | private readonly minioStorageService: MinioStorageService, 15 | private readonly logger: Logger, 16 | ) {} 17 | 18 | readonly sessionType: SessionType = SessionType.None 19 | 20 | readonly actionVersion: ActionVersion = ActionVersion.V1 21 | 22 | readonly name: string = 'testAcquirerProviderShareAppApp' 23 | 24 | readonly validationRules: ValidationSchema = { 25 | [DocumentType.InternalPassport]: { 26 | type: 'array', 27 | items: { 28 | type: 'object', 29 | props: { 30 | buffer: { type: 'buffer' }, 31 | originalname: { type: 'string' }, 32 | }, 33 | }, 34 | optional: true, 35 | }, 36 | [DocumentType.ForeignPassport]: { 37 | type: 'array', 38 | items: { 39 | type: 'object', 40 | props: { 41 | buffer: { type: 'buffer' }, 42 | originalname: { type: 'string' }, 43 | }, 44 | }, 45 | optional: true, 46 | }, 47 | encodeData: { type: 'string' }, 48 | } 49 | 50 | async handler(args: CustomActionArguments): Promise { 51 | this.logger.debug('Receive encrypted data from DIIA app in sharing app-app flow') 52 | const { params = {} } = args 53 | const internalPassports: File[] = params[DocumentType.InternalPassport] || [] 54 | const foreignPassports: File[] = params[DocumentType.ForeignPassport] || [] 55 | const success = true 56 | 57 | await this.minioStorageService.uploadFiles([...internalPassports, ...foreignPassports]) 58 | 59 | return { success } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { ActionSession, SessionType } from '@diia-inhouse/types' 2 | 3 | import { Request, ResponseError } from '@interfaces/index' 4 | 5 | export default { 6 | calculateResponseTime(req: Request): number { 7 | let time = 0 8 | if (req.$startTime) { 9 | const diff: [number, number] = process.hrtime(req.$startTime) 10 | 11 | time = Math.round((diff[0] + diff[1] / 1e9) * 1000) 12 | } 13 | 14 | return time 15 | }, 16 | 17 | handleValidationError(err: ResponseError): void { 18 | if (err.name !== 'ValidationError' || !Array.isArray(err.data)) { 19 | return 20 | } 21 | 22 | for (const error of err.data) { 23 | if (!error.field || error.field.slice(0, 7) !== 'params.') { 24 | continue 25 | } 26 | 27 | const fieldName: string = error.field 28 | 29 | // eslint-disable-next-line no-param-reassign 30 | error.field = error.field.slice(7) 31 | if (typeof error.message === 'string') { 32 | // eslint-disable-next-line no-param-reassign 33 | error.message = error.message.replace(fieldName, error.field) 34 | } 35 | } 36 | }, 37 | 38 | isErrorCode(processCode: number | undefined): boolean { 39 | return processCode?.toString().length === 4 40 | }, 41 | 42 | isResponseError(err: unknown): err is ResponseError { 43 | const isObject = typeof err === 'object' && err !== null 44 | 45 | return isObject && 'code' in err && 'message' in err 46 | }, 47 | 48 | isError(err: unknown): err is Error { 49 | return err instanceof Error 50 | }, 51 | 52 | isErrorWithCode(err: unknown): err is Error & { code: number } { 53 | return this.isError(err) && 'code' in err 54 | }, 55 | 56 | isEResidentContext(session?: ActionSession, path?: string): boolean { 57 | return ( 58 | (session && 'user' in session && [SessionType.EResident, SessionType.EResidentApplicant].includes(session.sessionType)) || 59 | Boolean(path?.startsWith('/e-resident')) 60 | ) 61 | }, 62 | 63 | isCabinetUserContext(session?: ActionSession, path?: string): boolean { 64 | return (session && 'user' in session && session.sessionType === SessionType.CabinetUser) || Boolean(path?.startsWith('/cabinet/')) 65 | }, 66 | } 67 | -------------------------------------------------------------------------------- /src/interfaces/routes/appRoute.ts: -------------------------------------------------------------------------------- 1 | import { Field } from 'multer' 2 | 3 | import { Env } from '@diia-inhouse/env' 4 | import { ActionVersion, HttpMethod, PortalUserPetitionPermissions, PortalUserPollPermissions, SessionType } from '@diia-inhouse/types' 5 | 6 | import { ExternalAlias, MimeType, Proxy, Route, RouteHeaderRawName } from '@interfaces/index' 7 | import { ProfileFeatureExpression } from '@interfaces/profileFeature' 8 | import { ExternalEvent } from '@interfaces/queue' 9 | import { PartnerScopes } from '@interfaces/services/partner' 10 | 11 | // in bytes 12 | export enum FileSize { 13 | MB_20 = 20971520, 14 | MB_1 = 1048576, 15 | MB_5 = 5242880, 16 | KB_500 = 524288, 17 | } 18 | 19 | export interface CustomHeader { 20 | name: RouteHeaderRawName 21 | versions: ActionVersion[] 22 | } 23 | 24 | export type SessionTypes = ( 25 | | SessionType.None 26 | | SessionType.User 27 | | SessionType.EResident 28 | | SessionType.EResidentApplicant 29 | | SessionType.CabinetUser 30 | )[] 31 | 32 | export interface RouteAuthParams { 33 | sessionType?: SessionType 34 | sessionTypes?: SessionTypes 35 | version: ActionVersion 36 | blockAccess?: boolean 37 | skipJwtVerification?: boolean 38 | scopes?: PartnerScopes 39 | permissions?: { 40 | petition?: PortalUserPetitionPermissions[] 41 | poll?: PortalUserPollPermissions[] 42 | } 43 | } 44 | 45 | export interface UploadAttempts { 46 | periodSec: number 47 | max: number 48 | } 49 | 50 | export interface AppRoute extends Route { 51 | method: HttpMethod 52 | path: string 53 | proxyTo?: Proxy 54 | action?: string 55 | externalAlias?: ExternalAlias 56 | auth: RouteAuthParams[] 57 | mergeParams?: boolean 58 | headers?: CustomHeader[] 59 | redirect?: string 60 | upload?: { 61 | allowedMimeTypes: MimeType[] 62 | field?: string 63 | required: boolean 64 | maxFileSize?: FileSize 65 | multiple?: boolean 66 | fields?: Field[] 67 | attempts?: UploadAttempts 68 | } 69 | forbiddenEnvs?: Env[] 70 | externalEvent?: ExternalEvent 71 | metadata?: { 72 | tags?: string[] 73 | summary?: string 74 | deprecated?: boolean 75 | } 76 | profileFeaturesExpression?: ProfileFeatureExpression 77 | preserveRawBody?: boolean 78 | } 79 | 80 | export interface GatewayUserActivityEventPayload { 81 | userIdentifier: string 82 | mobileUid: string 83 | } 84 | -------------------------------------------------------------------------------- /src/routes/documents/serviceEntrance.ts: -------------------------------------------------------------------------------- 1 | import { ActionVersion, HttpMethod, SessionType } from '@diia-inhouse/types' 2 | 3 | import { RouteHeaderRawName } from '@interfaces/index' 4 | import { AppRoute } from '@interfaces/routes/appRoute' 5 | 6 | enum DocumentsActions { 7 | VerifyServiceEntranceDocument = 'verifyServiceEntranceDocument', 8 | VerifyServiceEntranceDocumentByData = 'verifyServiceEntranceDocumentByData', 9 | } 10 | 11 | const routes: AppRoute[] = [ 12 | { 13 | method: HttpMethod.GET, 14 | path: '/api/:apiVersion/documents/:documentType/service-entrance/verify', 15 | action: DocumentsActions.VerifyServiceEntranceDocument, 16 | auth: [ 17 | { sessionType: SessionType.ServiceEntrance, version: ActionVersion.V1 }, 18 | { sessionType: SessionType.ServiceEntrance, version: ActionVersion.V2 }, 19 | ], 20 | headers: [ 21 | { name: RouteHeaderRawName.MOBILE_UID, versions: [ActionVersion.V1, ActionVersion.V2] }, 22 | { name: RouteHeaderRawName.APP_VERSION, versions: [ActionVersion.V1, ActionVersion.V2] }, 23 | { name: RouteHeaderRawName.PLATFORM_TYPE, versions: [ActionVersion.V1, ActionVersion.V2] }, 24 | { name: RouteHeaderRawName.TOKEN, versions: [ActionVersion.V1, ActionVersion.V2] }, 25 | { name: RouteHeaderRawName.PLATFORM_VERSION, versions: [ActionVersion.V1, ActionVersion.V2] }, 26 | ], 27 | metadata: { 28 | tags: ['Service Entrance'], 29 | }, 30 | }, 31 | { 32 | method: HttpMethod.POST, 33 | path: '/api/:apiVersion/documents/service-entrance/verify', 34 | action: DocumentsActions.VerifyServiceEntranceDocumentByData, 35 | auth: [ 36 | { sessionType: SessionType.ServiceEntrance, version: ActionVersion.V1 }, 37 | { sessionType: SessionType.ServiceEntrance, version: ActionVersion.V2 }, 38 | ], 39 | headers: [ 40 | { name: RouteHeaderRawName.MOBILE_UID, versions: [ActionVersion.V1, ActionVersion.V2] }, 41 | { name: RouteHeaderRawName.APP_VERSION, versions: [ActionVersion.V1, ActionVersion.V2] }, 42 | { name: RouteHeaderRawName.PLATFORM_TYPE, versions: [ActionVersion.V1, ActionVersion.V2] }, 43 | { name: RouteHeaderRawName.TOKEN, versions: [ActionVersion.V1, ActionVersion.V2] }, 44 | { name: RouteHeaderRawName.PLATFORM_VERSION, versions: [ActionVersion.V1, ActionVersion.V2] }, 45 | ], 46 | metadata: { 47 | tags: ['Service Entrance'], 48 | }, 49 | }, 50 | ] 51 | 52 | export default routes 53 | -------------------------------------------------------------------------------- /tests/unit/actions/v1/unauthorizedSimulate.spec.ts: -------------------------------------------------------------------------------- 1 | import DiiaLogger from '@diia-inhouse/diia-logger' 2 | import { EnvService } from '@diia-inhouse/env' 3 | import { UnauthorizedError } from '@diia-inhouse/errors' 4 | import { CacheService, RedisConfig } from '@diia-inhouse/redis' 5 | import TestKit, { mockClass } from '@diia-inhouse/test' 6 | 7 | import UnauthorizedSimulateAction from '@actions/v1/unauthorizedSimulate' 8 | 9 | import { CustomActionArguments } from '@interfaces/actions/v1/unauthorizedSimulate' 10 | 11 | const CacheServiceMock = mockClass(CacheService) 12 | 13 | describe(`Action ${UnauthorizedSimulateAction.constructor.name}`, () => { 14 | const testKit = new TestKit() 15 | const cacheService = new CacheServiceMock({}, {}, {}) 16 | const unauthorizedSimulateAction = new UnauthorizedSimulateAction(cacheService) 17 | 18 | describe('Method `handler`', () => { 19 | it('should return status ok', async () => { 20 | const headers = testKit.session.getHeaders() 21 | const args: CustomActionArguments = { 22 | headers, 23 | session: testKit.session.getUserSession(), 24 | } 25 | const redisKey = unauthorizedSimulateAction.cachePrefix + args.session.user.mobileUid 26 | 27 | jest.spyOn(cacheService, 'get').mockResolvedValueOnce('0') 28 | jest.spyOn(cacheService, 'set').mockResolvedValueOnce('true') 29 | 30 | expect(await unauthorizedSimulateAction.handler(args)).toEqual({ status: 'ok' }) 31 | expect(cacheService.get).toHaveBeenCalledWith(redisKey) 32 | expect(cacheService.set).toHaveBeenCalledWith(redisKey, '1') 33 | }) 34 | it('should fail with error in case is unauth', async () => { 35 | const headers = testKit.session.getHeaders() 36 | const args: CustomActionArguments = { 37 | headers, 38 | session: testKit.session.getUserSession(), 39 | } 40 | const redisKey = unauthorizedSimulateAction.cachePrefix + args.session.user.mobileUid 41 | 42 | jest.spyOn(cacheService, 'get').mockResolvedValueOnce('1') 43 | jest.spyOn(cacheService, 'set').mockResolvedValueOnce('true') 44 | 45 | await expect(async () => { 46 | await unauthorizedSimulateAction.handler(args) 47 | }).rejects.toEqual(new UnauthorizedError()) 48 | expect(cacheService.get).toHaveBeenCalledWith(redisKey) 49 | expect(cacheService.set).toHaveBeenCalledWith(redisKey, '0') 50 | }) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /src/routes/analytics.ts: -------------------------------------------------------------------------------- 1 | import { ActionVersion, HttpMethod, SessionType } from '@diia-inhouse/types' 2 | 3 | import { RouteHeaderRawName } from '@interfaces/index' 4 | import { AppRoute } from '@interfaces/routes/appRoute' 5 | 6 | enum AnalyticsActions { 7 | LogAppStatus = 'logAppStatus', 8 | SaveRating = 'saveRating', 9 | GetStaticRatingForm = 'getStaticRatingForm', 10 | } 11 | 12 | const routes: AppRoute[] = [ 13 | { 14 | method: HttpMethod.POST, 15 | path: '/api/:apiVersion/analytics/app-status', 16 | action: AnalyticsActions.LogAppStatus, 17 | auth: [{ sessionType: SessionType.None, version: ActionVersion.V1 }], 18 | headers: [ 19 | { name: RouteHeaderRawName.MOBILE_UID, versions: [ActionVersion.V1] }, 20 | { name: RouteHeaderRawName.APP_VERSION, versions: [ActionVersion.V1] }, 21 | { name: RouteHeaderRawName.PLATFORM_TYPE, versions: [ActionVersion.V1] }, 22 | { name: RouteHeaderRawName.PLATFORM_VERSION, versions: [ActionVersion.V1] }, 23 | { name: RouteHeaderRawName.TOKEN, versions: [ActionVersion.V1] }, 24 | ], 25 | }, 26 | { 27 | method: HttpMethod.POST, 28 | path: '/api/:apiVersion/analytics/service-rating/:category/:serviceCode/send', 29 | action: AnalyticsActions.SaveRating, 30 | auth: [{ sessionType: SessionType.User, version: ActionVersion.V1 }], 31 | headers: [ 32 | { name: RouteHeaderRawName.MOBILE_UID, versions: [ActionVersion.V1] }, 33 | { name: RouteHeaderRawName.APP_VERSION, versions: [ActionVersion.V1] }, 34 | { name: RouteHeaderRawName.PLATFORM_TYPE, versions: [ActionVersion.V1] }, 35 | { name: RouteHeaderRawName.PLATFORM_VERSION, versions: [ActionVersion.V1] }, 36 | { name: RouteHeaderRawName.TOKEN, versions: [ActionVersion.V1] }, 37 | ], 38 | }, 39 | { 40 | method: HttpMethod.GET, 41 | path: '/api/:apiVersion/analytics/service-rating/:category/:serviceCode/rating-form', 42 | action: AnalyticsActions.GetStaticRatingForm, 43 | auth: [{ sessionType: SessionType.User, version: ActionVersion.V1 }], 44 | headers: [ 45 | { name: RouteHeaderRawName.MOBILE_UID, versions: [ActionVersion.V1] }, 46 | { name: RouteHeaderRawName.APP_VERSION, versions: [ActionVersion.V1] }, 47 | { name: RouteHeaderRawName.PLATFORM_TYPE, versions: [ActionVersion.V1] }, 48 | { name: RouteHeaderRawName.PLATFORM_VERSION, versions: [ActionVersion.V1] }, 49 | { name: RouteHeaderRawName.TOKEN, versions: [ActionVersion.V1] }, 50 | ], 51 | }, 52 | ] 53 | 54 | export default routes 55 | -------------------------------------------------------------------------------- /src/routes/conference.ts: -------------------------------------------------------------------------------- 1 | import { ActionVersion, HttpMethod, SessionType } from '@diia-inhouse/types' 2 | 3 | import { RouteHeaderRawName } from '@src/interfaces' 4 | import { DEFAULT_USER_AUTH, DEFAULT_USER_HEADERS } from '@src/routes/defaults' 5 | 6 | import { AppRoute } from '@interfaces/routes/appRoute' 7 | 8 | const routes: AppRoute[] = [ 9 | { 10 | method: HttpMethod.POST, 11 | path: '/api/:apiVersion/conference', 12 | proxyTo: { 13 | serviceId: 'conference-service', 14 | }, 15 | auth: DEFAULT_USER_AUTH, 16 | headers: DEFAULT_USER_HEADERS, 17 | metadata: { 18 | tags: ['Conference'], 19 | }, 20 | }, 21 | { 22 | method: HttpMethod.GET, 23 | path: '/api/:apiVersion/conference/:conferenceId', 24 | proxyTo: { 25 | serviceId: 'conference-service', 26 | }, 27 | auth: DEFAULT_USER_AUTH, 28 | headers: DEFAULT_USER_HEADERS, 29 | metadata: { 30 | tags: ['Conference'], 31 | }, 32 | }, 33 | { 34 | method: HttpMethod.GET, 35 | path: '/api/:apiVersion/conferences', 36 | proxyTo: { 37 | serviceId: 'conference-service', 38 | }, 39 | auth: DEFAULT_USER_AUTH, 40 | headers: DEFAULT_USER_HEADERS, 41 | metadata: { 42 | tags: ['Conference'], 43 | }, 44 | }, 45 | { 46 | method: HttpMethod.POST, 47 | path: '/api/:apiVersion/conference/join', 48 | proxyTo: { 49 | serviceId: 'conference-service', 50 | }, 51 | auth: DEFAULT_USER_AUTH, 52 | headers: DEFAULT_USER_HEADERS, 53 | metadata: { 54 | tags: ['Conference'], 55 | }, 56 | }, 57 | { 58 | method: HttpMethod.POST, 59 | path: '/api/:apiVersion/conference/join', 60 | proxyTo: { 61 | serviceId: 'conference-service', 62 | }, 63 | auth: DEFAULT_USER_AUTH, 64 | headers: DEFAULT_USER_HEADERS, 65 | metadata: { 66 | tags: ['Conference'], 67 | }, 68 | }, 69 | { 70 | method: HttpMethod.POST, 71 | path: '/api/:apiVersion/conference/webex/webhook', 72 | proxyTo: { 73 | serviceId: 'conference-service', 74 | proxyHeaders: [RouteHeaderRawName.WEBEX_WEBHOOK_SIGNATURE], 75 | }, 76 | auth: [{ sessionType: SessionType.None, version: ActionVersion.V1 }], 77 | headers: [{ name: RouteHeaderRawName.WEBEX_WEBHOOK_SIGNATURE, versions: [ActionVersion.V1] }], 78 | metadata: { 79 | tags: ['Conference'], 80 | }, 81 | preserveRawBody: true, 82 | }, 83 | ] 84 | 85 | export default routes 86 | -------------------------------------------------------------------------------- /src/deps.ts: -------------------------------------------------------------------------------- 1 | import formidable from 'formidable' 2 | import { once } from 'lodash' 3 | 4 | import { AwilixContainer, DepsFactoryFn, Lifetime, NameAndRegistrationPair, asClass, asFunction, asValue } from '@diia-inhouse/diia-app' 5 | 6 | import { CmsService } from '@diia-inhouse/cms' 7 | import { HttpDeps, HttpService } from '@diia-inhouse/http' 8 | import { HttpProtocol } from '@diia-inhouse/types' 9 | 10 | import OpenApiGenerator from './apiDocs/openApiGenerator' 11 | import openApiNodeConnectedEvent from './apiDocs/openApiNodeConnectedEvent' 12 | import ApiDocsRoute from './apiDocs/route' 13 | import apiService from './apiService' 14 | import RoutesBuilder from './routes' 15 | import HeaderValidation from './validation/header' 16 | 17 | import MongodbFaqProvider from '@src/providers/faq/mongodbFaqProvider' 18 | import StrapiFaqProvider from '@src/providers/faq/strapiFaqProvider' 19 | 20 | import ExternalEventListenersUtils from '@utils/externalEventListeners' 21 | import TrackingUtils from '@utils/tracking' 22 | 23 | import { AppDeps, InternalDeps } from '@interfaces/application' 24 | import { AppConfig } from '@interfaces/types/config' 25 | 26 | export default async (config: AppConfig): ReturnType> => { 27 | const { strapi } = config 28 | 29 | const internalDeps: NameAndRegistrationPair = { 30 | trackingUtils: asClass(TrackingUtils).singleton(), 31 | externalEventListenersUtils: asClass(ExternalEventListenersUtils).singleton(), 32 | routesBuilder: asClass(RoutesBuilder).singleton(), 33 | headerValidation: asClass(HeaderValidation).singleton(), 34 | apiService: asFunction(apiService).singleton(), 35 | apiDocsRoute: asClass(ApiDocsRoute).singleton(), 36 | openApiGenerator: asClass(OpenApiGenerator).singleton(), 37 | openApiNodeConnectedEvent: asFunction(openApiNodeConnectedEvent).singleton(), 38 | faqProvider: strapi.isEnabled ? asClass(StrapiFaqProvider).singleton() : asClass(MongodbFaqProvider).singleton(), 39 | lazyMoleculer: { 40 | resolve: (c: AwilixContainer) => once(() => c.resolve('moleculer')), 41 | lifetime: Lifetime.SINGLETON, 42 | }, 43 | formidable: asValue(formidable), 44 | } 45 | 46 | const httpDeps: NameAndRegistrationPair = { 47 | httpService: asClass(HttpService, { injector: () => ({ protocol: HttpProtocol.Http }) }).singleton(), 48 | httpsService: asClass(HttpService, { injector: () => ({ protocol: HttpProtocol.Https }) }).singleton(), 49 | } 50 | 51 | return { 52 | cms: asClass(CmsService, { injector: () => ({ cmsConfig: strapi }) }).singleton(), 53 | 54 | ...internalDeps, 55 | ...httpDeps, 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/services/minioStorage.ts: -------------------------------------------------------------------------------- 1 | import { Client, ItemBucketMetadata, UploadedObjectInfo } from 'minio' 2 | 3 | import { Logger } from '@diia-inhouse/types' 4 | 5 | import { File } from '@interfaces/index' 6 | import { AppConfig } from '@interfaces/types/config' 7 | 8 | export default class MinioStorageService { 9 | private isEnabled: boolean 10 | 11 | private bucketName = 'demo' 12 | 13 | private region = 'ukraine' 14 | 15 | private minioClient!: Client 16 | 17 | constructor( 18 | private readonly config: AppConfig, 19 | private readonly logger: Logger, 20 | ) { 21 | this.isEnabled = this.config.minio.isEnabled 22 | 23 | if (this.config.minio.isEnabled === true) { 24 | this.minioClient = new Client({ 25 | endPoint: this.config.minio.host, 26 | port: this.config.minio.port, 27 | useSSL: false, 28 | accessKey: this.config.minio.accessKey, 29 | secretKey: this.config.minio.secretKey, 30 | }) 31 | } 32 | } 33 | 34 | async uploadFile(file: Buffer, filename: string): Promise { 35 | if (!this.isEnabled) { 36 | return 37 | } 38 | 39 | await this.createBucket() 40 | 41 | const metaData: ItemBucketMetadata = { 42 | 'Content-Type': 'application/octet-stream', 43 | 'X-Amz-Meta-Testing': 1234, 44 | example: 5678, 45 | } 46 | 47 | try { 48 | const result: UploadedObjectInfo = await this.minioClient.putObject(this.bucketName, filename, Buffer.from(file), metaData) 49 | 50 | this.logger.info(`File [${filename}] uploaded successfully`, result) 51 | } catch (err) { 52 | this.logger.error(`Failed to upload file [${filename}]`, { err }) 53 | throw err 54 | } 55 | } 56 | 57 | async uploadFiles(files: File[]): Promise { 58 | if (!this.isEnabled) { 59 | return 60 | } 61 | 62 | const promises: Promise[] = [] 63 | 64 | for (const item of files) { 65 | promises.push(this.uploadFile(item.buffer, item.originalname)) 66 | } 67 | 68 | await Promise.all(promises) 69 | } 70 | 71 | private async createBucket(): Promise { 72 | const exists: boolean = await this.minioClient.bucketExists(this.bucketName) 73 | if (!exists) { 74 | try { 75 | await this.minioClient.makeBucket(this.bucketName, this.region) 76 | this.logger.info(`Bucket [${this.bucketName}] created successfully in region [${this.region}]`) 77 | } catch (err) { 78 | this.logger.error(`Failed to create bucket [${this.bucketName}]`, { err }) 79 | throw err 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/routes/documents/driverLicense.ts: -------------------------------------------------------------------------------- 1 | import { ActionVersion, HttpMethod, SessionType } from '@diia-inhouse/types' 2 | 3 | import { RouteHeaderRawName } from '@interfaces/index' 4 | import { AppRoute } from '@interfaces/routes/appRoute' 5 | 6 | enum DocumentsActions { 7 | GetDriverLicense = 'getDriverLicense', 8 | 9 | ShareDriverLicense = 'shareDriverLicense', 10 | VerifyDriverLicense = 'verifyDriverLicense', 11 | } 12 | 13 | const routes: AppRoute[] = [ 14 | { 15 | method: HttpMethod.GET, 16 | path: '/api/:apiVersion/documents/driver-license', 17 | action: DocumentsActions.GetDriverLicense, 18 | auth: [{ sessionType: SessionType.User, version: ActionVersion.V1 }], 19 | headers: [ 20 | { name: RouteHeaderRawName.MOBILE_UID, versions: [ActionVersion.V1] }, 21 | { name: RouteHeaderRawName.APP_VERSION, versions: [ActionVersion.V1] }, 22 | { name: RouteHeaderRawName.PLATFORM_TYPE, versions: [ActionVersion.V1] }, 23 | { name: RouteHeaderRawName.PLATFORM_VERSION, versions: [ActionVersion.V1] }, 24 | ], 25 | metadata: { 26 | tags: ['Get Documents From Registry'], 27 | }, 28 | }, 29 | { 30 | method: HttpMethod.GET, 31 | path: '/api/:apiVersion/documents/driver-license/:documentId/share', 32 | action: DocumentsActions.ShareDriverLicense, 33 | auth: [{ sessionType: SessionType.User, version: ActionVersion.V1 }], 34 | headers: [ 35 | { name: RouteHeaderRawName.MOBILE_UID, versions: [ActionVersion.V1] }, 36 | { name: RouteHeaderRawName.TOKEN, versions: [ActionVersion.V1] }, 37 | { name: RouteHeaderRawName.APP_VERSION, versions: [ActionVersion.V1] }, 38 | { name: RouteHeaderRawName.PLATFORM_TYPE, versions: [ActionVersion.V1] }, 39 | { name: RouteHeaderRawName.PLATFORM_VERSION, versions: [ActionVersion.V1] }, 40 | ], 41 | metadata: { 42 | tags: ['Share Document'], 43 | }, 44 | }, 45 | { 46 | method: HttpMethod.GET, 47 | path: '/api/:apiVersion/documents/driver-license/verify', 48 | action: DocumentsActions.VerifyDriverLicense, 49 | auth: [{ sessionType: SessionType.User, version: ActionVersion.V1, blockAccess: false }], 50 | headers: [ 51 | { name: RouteHeaderRawName.MOBILE_UID, versions: [ActionVersion.V1] }, 52 | { name: RouteHeaderRawName.TOKEN, versions: [ActionVersion.V1] }, 53 | { name: RouteHeaderRawName.APP_VERSION, versions: [ActionVersion.V1] }, 54 | { name: RouteHeaderRawName.PLATFORM_TYPE, versions: [ActionVersion.V1] }, 55 | { name: RouteHeaderRawName.PLATFORM_VERSION, versions: [ActionVersion.V1] }, 56 | ], 57 | metadata: { 58 | tags: ['Verify Document'], 59 | }, 60 | }, 61 | ] 62 | 63 | export default routes 64 | -------------------------------------------------------------------------------- /src/validation/files.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestError } from '@diia-inhouse/errors' 2 | 3 | import { File } from '@interfaces/index' 4 | import { AppRoute } from '@interfaces/routes/appRoute' 5 | import { ValidationPredicate } from '@interfaces/validation/sandbox' 6 | 7 | export class FilesValidator { 8 | static validateIfFileIsEmpty(route: AppRoute, file?: File): ValidationPredicate { 9 | return (): Error | undefined => { 10 | const files = file ? [file] : [] 11 | 12 | return FilesValidator.areFilesEmpty(files, route) 13 | } 14 | } 15 | 16 | static validateIfFileExceedsSize(route: AppRoute, file?: File): ValidationPredicate { 17 | return (): Error | undefined => { 18 | const files = file ? [file] : [] 19 | 20 | return FilesValidator.areFilesSizeExceeded(files, route) 21 | } 22 | } 23 | 24 | static validateIfFileMimeTypeIsAllowed(route: AppRoute, file?: File): ValidationPredicate { 25 | return (): Error | undefined => { 26 | const files = file ? [file] : [] 27 | 28 | return FilesValidator.areFilesMimeTypeAllowed(files, route) 29 | } 30 | } 31 | 32 | static validateIfFilesAreEmpty(files: File[], route: AppRoute): ValidationPredicate { 33 | return (): Error | undefined => FilesValidator.areFilesEmpty(files, route) 34 | } 35 | 36 | static validateIfOneOfFilesExceedSize(files: File[], route: AppRoute): ValidationPredicate { 37 | return (): Error | undefined => FilesValidator.areFilesSizeExceeded(files, route) 38 | } 39 | 40 | static validateIfFilesMimeTypeIsAllowed(files: File[], route: AppRoute): ValidationPredicate { 41 | return (): Error | undefined => FilesValidator.areFilesMimeTypeAllowed(files, route) 42 | } 43 | 44 | private static areFilesEmpty(files: File[], route: AppRoute): Error | undefined { 45 | const { required } = route.upload || {} 46 | 47 | if (files.length === 0 && required) { 48 | return new BadRequestError('The file is empty or exceeds file size') 49 | } 50 | } 51 | 52 | private static areFilesSizeExceeded(files: File[], route: AppRoute): Error | undefined { 53 | const { required, maxFileSize } = route.upload || {} 54 | 55 | for (const file of files) { 56 | if (required && maxFileSize && file && maxFileSize < file.size) { 57 | return new BadRequestError('File size exceeded', { size: file.size }) 58 | } 59 | } 60 | } 61 | 62 | private static areFilesMimeTypeAllowed(files: File[], route: AppRoute): Error | undefined { 63 | const { allowedMimeTypes = [] } = route.upload || {} 64 | 65 | for (const file of files) { 66 | if (file && !allowedMimeTypes.includes(file.mimetype)) { 67 | return new BadRequestError(`This mimetype [${file.mimetype}] is not allowed for this endpoint`) 68 | } 69 | } 70 | } 71 | } 72 | --------------------------------------------------------------------------------