diff --git a/.env.dist b/.env.dist index 2b50f2b..5f4b337 100644 --- a/.env.dist +++ b/.env.dist @@ -8,5 +8,5 @@ ALICE_BOB_PRIVATE_KEY= THREE_ROUTE_API_URL= THREE_ROUTE_API_AUTH_TOKEN= REDIS_URL= -ADD_NOTIFICATION_USERNAME= -ADD_NOTIFICATION_PASSWORD= +ADMIN_USERNAME= +ADMIN_PASSWORD= diff --git a/package.json b/package.json index 3d580b2..eb51313 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,10 @@ "pino-http": "^5.5.0", "pino-pretty": "^4.7.1", "qs": "^6.10.3", - "semaphore": "^1.1.0" + "semaphore": "^1.1.0", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.0", + "yup": "^1.3.2" }, "scripts": { "start": "cross-env NODE_ENV=development ts-node-dev --files --quiet src/index.ts", diff --git a/src/advertising/slise.ts b/src/advertising/slise.ts new file mode 100644 index 0000000..36638b9 --- /dev/null +++ b/src/advertising/slise.ts @@ -0,0 +1,153 @@ +import { redisClient } from '../redis'; +import { isDefined } from '../utils/helpers'; + +/** Style properties names that are likely to be unnecessary for banners are skipped */ +export const stylePropsNames = [ + 'align-content', + 'align-items', + 'align-self', + 'alignment-baseline', + 'aspect-ratio', + 'background', + 'border-radius', + 'bottom', + 'box-shadow', + 'box-sizing', + 'display', + 'flex', + 'flex-basis', + 'flex-direction', + 'flex-flow', + 'flex-grow', + 'flex-shrink', + 'flex-wrap', + 'float', + 'height', + 'justify-content', + 'justify-items', + 'justify-self', + 'left', + 'margin', + 'margin-block', + 'margin-block-end', + 'margin-block-start', + 'margin-bottom', + 'margin-inline', + 'margin-inline-end', + 'margin-inline-start', + 'margin-left', + 'margin-right', + 'margin-top', + 'max-block-size', + 'max-height', + 'max-inline-size', + 'max-width', + 'min-block-size', + 'min-height', + 'min-inline-size', + 'min-width', + 'overflow', + 'overflow-anchor', + 'overflow-wrap', + 'overflow-x', + 'overflow-y', + 'padding', + 'padding-block', + 'padding-block-end', + 'padding-block-start', + 'padding-bottom', + 'padding-inline', + 'padding-inline-end', + 'padding-inline-start', + 'padding-left', + 'padding-right', + 'padding-top', + 'position', + 'right', + 'text-align', + 'top', + 'visibility', + 'width', + 'z-index' +]; +export type StylePropName = (typeof stylePropsNames)[number]; + +interface SliseAdStylesOverrides { + parentDepth: number; + style: Record; +} + +export interface SliseAdPlacesRule { + urlRegexes: string[]; + selector: { + isMultiple: boolean; + cssString: string; + parentDepth: number; + shouldUseDivWrapper: boolean; + divWrapperStyle?: Record; + }; + stylesOverrides?: SliseAdStylesOverrides[]; +} + +export interface SliseAdProvidersByDomainRule { + urlRegexes: string[]; + providers: string[]; +} + +const SLISE_AD_PLACES_RULES_KEY = 'slise_ad_places_rules'; +const SLISE_AD_PROVIDERS_BY_SITES_KEY = 'slise_ad_providers_by_sites'; +const SLISE_AD_PROVIDERS_ALL_SITES_KEY = 'slise_ad_providers_all_sites'; +const SLISE_AD_PROVIDERS_LIST_KEY = 'slise_ad_providers_list'; + +const objectStorageMethodsFactory = (storageKey: string, fallbackValue: V) => ({ + getByKey: async (key: string): Promise => { + const value = await redisClient.hget(storageKey, key); + + return isDefined(value) ? JSON.parse(value) : fallbackValue; + }, + getAllValues: async (): Promise> => { + const values = await redisClient.hgetall(storageKey); + + const parsedValues: Record = {}; + for (const key in values) { + parsedValues[key] = JSON.parse(values[key]); + } + + return parsedValues; + }, + upsertValues: (newValues: Record) => + redisClient.hmset( + storageKey, + Object.fromEntries(Object.entries(newValues).map(([domain, value]) => [domain, JSON.stringify(value)])) + ), + removeValues: (keys: string[]) => redisClient.hdel(storageKey, ...keys) +}); + +export const { + getByKey: getSliseAdPlacesRulesByDomain, + getAllValues: getAllSliseAdPlacesRules, + upsertValues: upsertSliseAdPlacesRules, + removeValues: removeSliseAdPlacesRules +} = objectStorageMethodsFactory(SLISE_AD_PLACES_RULES_KEY, []); + +export const { + getByKey: getSliseAdProvidersByDomain, + getAllValues: getAllSliseAdProvidersBySites, + upsertValues: upsertSliseAdProvidersBySites, + removeValues: removeSliseAdProvidersBySites +} = objectStorageMethodsFactory(SLISE_AD_PROVIDERS_BY_SITES_KEY, []); + +export const { + getByKey: getSelectorsByProviderId, + getAllValues: getAllProviders, + upsertValues: upsertProviders, + removeValues: removeProviders +} = objectStorageMethodsFactory(SLISE_AD_PROVIDERS_LIST_KEY, []); + +export const getSliseAdProvidersForAllSites = async () => redisClient.smembers(SLISE_AD_PROVIDERS_ALL_SITES_KEY); + +export const addSliseAdProvidersForAllSites = async (providers: string[]) => + redisClient.sadd(SLISE_AD_PROVIDERS_ALL_SITES_KEY, ...providers); + +export const removeSliseAdProvidersForAllSites = async (providers: string[]) => + redisClient.srem(SLISE_AD_PROVIDERS_ALL_SITES_KEY, ...providers); diff --git a/src/config.ts b/src/config.ts index 4cfd913..7ed45ca 100644 --- a/src/config.ts +++ b/src/config.ts @@ -11,8 +11,8 @@ export const EnvVars = { THREE_ROUTE_API_URL: getEnv('THREE_ROUTE_API_URL'), THREE_ROUTE_API_AUTH_TOKEN: getEnv('THREE_ROUTE_API_AUTH_TOKEN'), REDIS_URL: getEnv('REDIS_URL'), - ADD_NOTIFICATION_USERNAME: getEnv('ADD_NOTIFICATION_USERNAME'), - ADD_NOTIFICATION_PASSWORD: getEnv('ADD_NOTIFICATION_PASSWORD') + ADMIN_USERNAME: getEnv('ADMIN_USERNAME'), + ADMIN_PASSWORD: getEnv('ADMIN_PASSWORD') }; for (const name in EnvVars) { diff --git a/src/index.ts b/src/index.ts index 1a87be7..6046687 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,8 @@ import express, { Request, Response } from 'express'; import firebaseAdmin from 'firebase-admin'; import { stdSerializers } from 'pino'; import pinoHttp from 'pino-http'; +import swaggerJSDoc from 'swagger-jsdoc'; +import swaggerUi from 'swagger-ui-express'; import { getAdvertisingInfo } from './advertising/advertising'; import { MIN_ANDROID_APP_VERSION, MIN_IOS_APP_VERSION } from './config'; @@ -17,6 +19,7 @@ import { getNotifications } from './notifications/utils/get-notifications.util'; import { getParsedContent } from './notifications/utils/get-parsed-content.util'; import { getPlatforms } from './notifications/utils/get-platforms.util'; import { redisClient } from './redis'; +import { sliseRulesRouter } from './routers/slise-ad-rules'; import { getABData } from './utils/ab-test'; import { cancelAliceBobOrder } from './utils/alice-bob/cancel-alice-bob-order'; import { createAliceBobOrder } from './utils/alice-bob/create-alice-bob-order'; @@ -322,6 +325,21 @@ app.get('/api/advertising-info', (_req, res) => { } }); +app.use('/api/slise-ad-rules', sliseRulesRouter); + +const swaggerOptions = { + swaggerDefinition: { + openapi: '3.0.0', + info: { + title: 'Temple Wallet backend', + version: '1.0.0' + } + }, + apis: ['./src/index.ts', './src/routers/**/*.ts'] +}; +const swaggerSpec = swaggerJSDoc(swaggerOptions); +app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); + // start the server listening for requests const port = Boolean(process.env.PORT) ? process.env.PORT : 3000; app.listen(port, () => console.info(`Server is running on port ${port}...`)); diff --git a/src/middlewares/basic-auth.middleware.ts b/src/middlewares/basic-auth.middleware.ts index a9d7ad5..deaca01 100644 --- a/src/middlewares/basic-auth.middleware.ts +++ b/src/middlewares/basic-auth.middleware.ts @@ -3,13 +3,19 @@ import { Request, Response, NextFunction } from 'express'; import { EnvVars } from '../config'; import { isDefined } from '../utils/helpers'; +const credentials = { + username: EnvVars.ADMIN_USERNAME, + password: EnvVars.ADMIN_PASSWORD +}; + export const basicAuth = (req: Request, res: Response, next: NextFunction) => { const base64EncodedCredentials = req.get('Authorization'); if (isDefined(base64EncodedCredentials)) { const [username, password] = Buffer.from(base64EncodedCredentials.split(' ')[1], 'base64').toString().split(':'); + const { username: correctUsername, password: correctPassword } = credentials; - if (!(username === EnvVars.ADD_NOTIFICATION_USERNAME && password === EnvVars.ADD_NOTIFICATION_PASSWORD)) { + if (!(username === correctUsername && password === correctPassword)) { handleNotAuthenticated(res, next); } next(); diff --git a/src/routers/slise-ad-rules/ad-places.ts b/src/routers/slise-ad-rules/ad-places.ts new file mode 100644 index 0000000..d8bc841 --- /dev/null +++ b/src/routers/slise-ad-rules/ad-places.ts @@ -0,0 +1,215 @@ +import { Router } from 'express'; + +import { + getAllSliseAdPlacesRules, + getSliseAdPlacesRulesByDomain, + removeSliseAdPlacesRules, + upsertSliseAdPlacesRules +} from '../../advertising/slise'; +import { addObjectStorageMethodsToRouter } from '../../utils/express-helpers'; +import { hostnamesListSchema, sliseAdPlacesRulesDictionarySchema } from '../../utils/schemas'; + +/** + * @swagger + * components: + * schemas: + * SliseAdPlacesRuleSelector: + * type: object + * required: + * - isMultiple + * - cssString + * - parentDepth + * - shouldUseDivWrapper + * properties: + * isMultiple: + * type: boolean + * description: Whether the selector should return multiple elements + * cssString: + * type: string + * description: CSS selector + * parentDepth: + * type: number + * min: 0 + * integer: true + * description: > + * Indicates the depth of the parent element of the selected element, i. e. 0 means that the selected + * elements are ads banners themselves, 1 means that the selected elements are ads banners' direct + * children and so on. + * shouldUseDivWrapper: + * type: boolean + * description: Whether the Slise ads banner should be wrapped in a div + * divWrapperStyle: + * type: object + * description: Style of the div wrapper + * additionalProperties: + * type: string + * SliseAdStylesOverrides: + * type: object + * required: + * - parentDepth + * - style + * properties: + * parentDepth: + * type: number + * min: 0 + * integer: true + * description: > + * Indicates the depth of the parent element for an ad banner that should change its style. + * style: + * type: object + * description: New style of the parent element + * additionalProperties: + * type: string + * SliseAdPlacesRule: + * type: object + * required: + * - urlRegexes + * - selector + * properties: + * urlRegexes: + * type: array + * items: + * type: string + * format: regex + * selector: + * $ref: '#/components/schemas/SliseAdPlacesRuleSelector' + * stylesOverrides: + * type: array + * items: + * $ref: '#/components/schemas/SliseAdStylesOverrides' + * example: + * urlRegexes: + * - '^https://goerli\.etherscan\.io/?$' + * selector: + * isMultiple: false + * cssString: 'main > section div.row > div:nth-child(2) > div' + * parentDepth: 0 + * shouldUseDivWrapper: false + * SliseAdPlacesRulesDictionary: + * type: object + * additionalProperties: + * type: array + * items: + * $ref: '#/components/schemas/SliseAdPlacesRule' + * example: + * goerli.etherscan.io: + * - urlRegexes: + * - '^https://goerli\.etherscan\.io/?$' + * selector: + * isMultiple: false + * cssString: 'main > section div.row > div:nth-child(2) > div' + * parentDepth: 0 + * shouldUseDivWrapper: false + * www.dextools.io: + * - urlRegexes: + * - '^https://www\.dextools\.io/app/[A-z]{2}/[0-9A-z-]+/pair-explorer' + * selector: + * isMultiple: true + * cssString: 'app-header-banner' + * parentDepth: 1 + * shouldUseDivWrapper: false + * - urlRegexes: + * - '^https://www\.dextools\.io/app/[A-z]{2}/[0-9A-z-]+/pairs' + * selector: + * isMultiple: false + * cssString: 'div.left-container > app-pe-banner:nth-child(2)' + * parentDepth: 0 + * shouldUseDivWrapper: true + */ + +export const sliseAdPlacesRulesRouter = Router(); + +/** + * @swagger + * /api/slise-ad-rules/ad-places/{domain}: + * get: + * summary: Get rules for ads places for the specified domain + * parameters: + * - in: path + * name: domain + * required: true + * schema: + * type: string + * format: hostname + * example: 'goerli.etherscan.io' + * responses: + * '200': + * description: Rules list + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/SliseAdPlacesRule' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * /api/slise-ad-rules/ad-places: + * get: + * summary: Get all rules for ads places + * responses: + * '200': + * description: Domain - rules list dictionary + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SliseAdPlacesRulesDictionary' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * post: + * summary: Add rules for ads places. If rules for a domain already exist, they will be overwritten + * security: + * - basicAuth: [] + * requestBody: + * description: Domain - rules list dictionary + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SliseAdPlacesRulesDictionary' + * responses: + * '200': + * $ref: '#/components/responses/SuccessResponse' + * '400': + * $ref: '#/components/responses/ErrorResponse' + * '401': + * $ref: '#/components/responses/UnauthorizedError' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * delete: + * summary: Remove rules for ads places + * security: + * - basicAuth: [] + * requestBody: + * description: List of domain names to remove rules for + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + * format: hostname + * example: + * - 'goerli.etherscan.io' + * responses: + * '200': + * $ref: '#/components/responses/SuccessResponse' + * '400': + * $ref: '#/components/responses/ErrorResponse' + * '401': + * $ref: '#/components/responses/UnauthorizedError' + * '500': + * $ref: '#/components/responses/ErrorResponse' + */ +addObjectStorageMethodsToRouter( + sliseAdPlacesRulesRouter, + '/', + { + getByKey: getSliseAdPlacesRulesByDomain, + getAllValues: getAllSliseAdPlacesRules, + upsertValues: upsertSliseAdPlacesRules, + removeValues: removeSliseAdPlacesRules + }, + 'domain', + sliseAdPlacesRulesDictionarySchema, + hostnamesListSchema, + entriesCount => `${entriesCount} entries have been removed` +); diff --git a/src/routers/slise-ad-rules/index.ts b/src/routers/slise-ad-rules/index.ts new file mode 100644 index 0000000..0dca519 --- /dev/null +++ b/src/routers/slise-ad-rules/index.ts @@ -0,0 +1,43 @@ +import { Router } from 'express'; + +import { sliseAdPlacesRulesRouter } from './ad-places'; +import { sliseAdProvidersRouter } from './providers'; + +/** + * @swagger + * components: + * securitySchemes: + * basicAuth: + * type: http + * scheme: basic + * responses: + * UnauthorizedError: + * description: Authentication information is missing or invalid + * headers: + * WWW_Authenticate: + * schema: + * type: string + * ErrorResponse: + * description: Error response + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * SuccessResponse: + * description: Success response + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + */ + +export const sliseRulesRouter = Router(); + +sliseRulesRouter.use('/ad-places', sliseAdPlacesRulesRouter); +sliseRulesRouter.use('/providers', sliseAdProvidersRouter); diff --git a/src/routers/slise-ad-rules/providers.ts b/src/routers/slise-ad-rules/providers.ts new file mode 100644 index 0000000..c7056bd --- /dev/null +++ b/src/routers/slise-ad-rules/providers.ts @@ -0,0 +1,372 @@ +import { Router } from 'express'; + +import { + addSliseAdProvidersForAllSites, + getAllProviders, + getAllSliseAdProvidersBySites, + getSelectorsByProviderId, + getSliseAdProvidersByDomain, + getSliseAdProvidersForAllSites, + removeProviders, + removeSliseAdProvidersBySites, + removeSliseAdProvidersForAllSites, + upsertProviders, + upsertSliseAdProvidersBySites +} from '../../advertising/slise'; +import { basicAuth } from '../../middlewares/basic-auth.middleware'; +import { addObjectStorageMethodsToRouter, withBodyValidation, withExceptionHandler } from '../../utils/express-helpers'; +import { + adTypesListSchema, + hostnamesListSchema, + sliseAdProvidersByDomainsRulesDictionarySchema, + sliseAdProvidersDictionarySchema +} from '../../utils/schemas'; + +/** + * @swagger + * components: + * schemas: + * SliseAdProvidersByDomainRule: + * type: object + * required: + * - urlRegexes + * - providers + * properties: + * urlRegexes: + * type: array + * items: + * type: string + * format: regex + * providers: + * type: array + * items: + * type: string + * example: + * urlRegexes: + * - '^https://polygonscan\.com/?$' + * providers: + * - 'coinzilla' + * - 'bitmedia' + * SliseAdProvidersByDomainsRulesDictionary: + * type: object + * additionalProperties: + * type: array + * items: + * $ref: '#/components/schemas/SliseAdProvidersByDomainRule' + * example: + * polygonscan.com: + * - urlRegexes: + * - '^https://polygonscan\.com/?$' + * providers: + * - 'coinzilla' + * - 'bitmedia' + * SliseAdProvidersDictionary: + * type: object + * additionalProperties: + * type: array + * items: + * type: string + * example: + * google: + * - '#Ads_google_bottom_wide' + * - '.GoogleAdInfo' + * - 'a[href^="https://googleads.g.doubleclick.net/pcs/click"]' + */ + +export const sliseAdProvidersRouter = Router(); + +/** + * @swagger + * /api/slise-ad-rules/providers/all-sites: + * get: + * summary: Get providers of ads for which ads should be replaced at all sites + * responses: + * '200': + * description: List of providers + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + * example: + * - 'coinzilla' + * - 'bitmedia' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * post: + * summary: > + * Add providers of ads for which ads should be replaced at all sites. They will not be removed + * from lists of providers from specific sites. Checks for providers existence are not performed + * security: + * - basicAuth: [] + * requestBody: + * description: List of providers + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + * example: + * - 'coinzilla' + * - 'bitmedia' + * responses: + * '200': + * $ref: '#/components/responses/SuccessResponse' + * '400': + * $ref: '#/components/responses/ErrorResponse' + * '401': + * $ref: '#/components/responses/UnauthorizedError' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * delete: + * summary: Remove providers of ads for which ads should be replaced at all sites + * security: + * - basicAuth: [] + * requestBody: + * description: List of providers + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + * example: + * - 'coinzilla' + * - 'bitmedia' + * responses: + * '200': + * $ref: '#/components/responses/SuccessResponse' + * '400': + * $ref: '#/components/responses/ErrorResponse' + * '401': + * $ref: '#/components/responses/UnauthorizedError' + * '500': + * $ref: '#/components/responses/ErrorResponse' + */ +sliseAdProvidersRouter + .route('/all-sites') + .get( + withExceptionHandler(async (_req, res) => { + const providers = await getSliseAdProvidersForAllSites(); + + res.status(200).send(providers); + }) + ) + .post( + basicAuth, + withExceptionHandler( + withBodyValidation(adTypesListSchema, async (req, res) => { + const providersAddedCount = await addSliseAdProvidersForAllSites(req.body); + + res.status(200).send({ message: `${providersAddedCount} providers have been added` }); + }) + ) + ) + .delete( + basicAuth, + withExceptionHandler( + withBodyValidation(adTypesListSchema, async (req, res) => { + const providersRemovedCount = await removeSliseAdProvidersForAllSites(req.body); + + res.status(200).send({ message: `${providersRemovedCount} providers have been removed` }); + }) + ) + ); + +/** + * @swagger + * /api/slise-ad-rules/providers/by-sites/{domain}: + * get: + * summary: Get rules for providers of ads for which ads should be replaced at the specified site + * parameters: + * - in: path + * name: domain + * required: true + * schema: + * type: string + * format: hostname + * example: 'goerli.etherscan.io' + * responses: + * '200': + * description: Rules list + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/SliseAdProvidersByDomainRule' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * /api/slise-ad-rules/providers/by-sites: + * get: + * summary: Get rules for providers of ads for which ads should be replaced at all sites + * responses: + * '200': + * description: Domain - rules list dictionary + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SliseAdProvidersByDomainsRulesDictionary' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * post: + * summary: > + * Add rules for providers of ads for the specified sites. They will not be removed from lists + * of providers from all sites. Checks for providers existence are not performed + * security: + * - basicAuth: [] + * requestBody: + * description: Domain - rules list dictionary + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SliseAdProvidersByDomainsRulesDictionary' + * responses: + * '200': + * $ref: '#/components/responses/SuccessResponse' + * '400': + * $ref: '#/components/responses/ErrorResponse' + * '401': + * $ref: '#/components/responses/UnauthorizedError' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * delete: + * summary: Remove rules for providers of ads for the specified sites + * security: + * - basicAuth: [] + * requestBody: + * description: List of domains + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + * format: hostname + * example: + * - 'goerli.etherscan.io' + * - 'polygonscan.com' + * responses: + * '200': + * $ref: '#/components/responses/SuccessResponse' + * '400': + * $ref: '#/components/responses/ErrorResponse' + * '401': + * $ref: '#/components/responses/UnauthorizedError' + * '500': + * $ref: '#/components/responses/ErrorResponse' + */ +addObjectStorageMethodsToRouter( + sliseAdProvidersRouter, + '/by-sites', + { + getAllValues: getAllSliseAdProvidersBySites, + getByKey: getSliseAdProvidersByDomain, + upsertValues: upsertSliseAdProvidersBySites, + removeValues: removeSliseAdProvidersBySites + }, + 'domain', + sliseAdProvidersByDomainsRulesDictionarySchema, + hostnamesListSchema, + entriesCount => `${entriesCount} entries have been removed` +); + +/** + * @swagger + * /api/slise-ad-rules/providers/{providerId}: + * get: + * summary: Get selectors for a provider + * parameters: + * - in: path + * name: providerId + * required: true + * schema: + * type: string + * example: 'google' + * responses: + * '200': + * description: List of CSS selectors + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + * example: + * - '#Ads_google_bottom_wide' + * - '.GoogleAdInfo' + * - 'a[href^="https://googleads.g.doubleclick.net/pcs/click"]' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * /api/slise-ad-rules/providers: + * get: + * summary: Get all providers + * responses: + * '200': + * description: Provider - selectors dictionary + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SliseAdProvidersDictionary' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * post: + * summary: Upserts providers. Providers that have existed before will be overwritten + * security: + * - basicAuth: [] + * requestBody: + * description: Provider - selectors dictionary + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SliseAdProvidersDictionary' + * responses: + * '200': + * $ref: '#/components/responses/SuccessResponse' + * '400': + * $ref: '#/components/responses/ErrorResponse' + * '401': + * $ref: '#/components/responses/UnauthorizedError' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * delete: + * summary: Delete specified providers. Cascade delete rules are not applied + * security: + * - basicAuth: [] + * requestBody: + * description: List of provider IDs to delete + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + * example: + * - 'coinzilla' + * - 'bitmedia' + * responses: + * '200': + * $ref: '#/components/responses/SuccessResponse' + * '400': + * $ref: '#/components/responses/ErrorResponse' + * '401': + * $ref: '#/components/responses/UnauthorizedError' + * '500': + * $ref: '#/components/responses/ErrorResponse' + */ +addObjectStorageMethodsToRouter( + sliseAdProvidersRouter, + '/', + { + getAllValues: getAllProviders, + getByKey: getSelectorsByProviderId, + upsertValues: upsertProviders, + removeValues: removeProviders + }, + 'providerId', + sliseAdProvidersDictionarySchema, + adTypesListSchema, + entriesCount => `${entriesCount} providers have been removed` +); diff --git a/src/utils/express-helpers.ts b/src/utils/express-helpers.ts new file mode 100644 index 0000000..8a6e05b --- /dev/null +++ b/src/utils/express-helpers.ts @@ -0,0 +1,98 @@ +import { NextFunction, Request, RequestHandler, Response, Router } from 'express'; +import { ArraySchema as IArraySchema, ObjectSchema as IObjectSchema, Schema, ValidationError } from 'yup'; + +import { basicAuth } from '../middlewares/basic-auth.middleware'; +import logger from './logger'; + +interface ObjectStorageMethods { + getByKey: (key: string) => Promise; + getAllValues: () => Promise>; + upsertValues: (newValues: Record) => Promise<'OK'>; + removeValues: (keys: string[]) => Promise; +} + +type TypedBodyRequestHandler = ( + req: Request, unknown, T>, + res: Response, + next: NextFunction +) => void; + +export const withBodyValidation = + (schema: Schema, handler: TypedBodyRequestHandler): RequestHandler => + async (req, res, next) => { + try { + req.body = await schema.validate(req.body); + } catch (error) { + if (error instanceof ValidationError) { + return res.status(400).send({ error: error.message }); + } + + throw error; + } + + return handler(req, res, next); + }; + +export const withExceptionHandler = + (handler: RequestHandler): RequestHandler => + async (req, res, next) => { + try { + await handler(req, res, next); + } catch (error) { + logger.error(error as object); + res.status(500).send({ error }); + } + }; + +export const addObjectStorageMethodsToRouter = ( + router: Router, + path: string, + methods: ObjectStorageMethods, + keyName: string, + objectValidationSchema: IObjectSchema>, + keysArrayValidationSchema: IArraySchema, + successfulRemovalMessage: (removedEntriesCount: number) => string +) => { + router.get( + path === '/' ? `/:${keyName}` : `${path}/:${keyName}`, + withExceptionHandler(async (req, res) => { + const { [keyName]: key } = req.params; + + const value = await methods.getByKey(key); + + res.status(200).send(value); + }) + ); + + router + .route(path) + .get( + withExceptionHandler(async (_req, res) => { + const values = await methods.getAllValues(); + + res.status(200).send(values); + }) + ) + .post( + basicAuth, + withExceptionHandler( + withBodyValidation(objectValidationSchema, async (req, res) => { + const validatedValues = req.body; + + await methods.upsertValues(validatedValues); + + res.status(200).send({ message: 'Values have been added successfully' }); + }) + ) + ) + .delete( + basicAuth, + withExceptionHandler( + withBodyValidation(keysArrayValidationSchema, async (req, res) => { + const removedEntriesCount = await methods.removeValues(req.body); + + res.status(200).send({ message: successfulRemovalMessage(removedEntriesCount) }); + }) + ) + ); +}; diff --git a/src/utils/schemas.ts b/src/utils/schemas.ts new file mode 100644 index 0000000..5f887b4 --- /dev/null +++ b/src/utils/schemas.ts @@ -0,0 +1,139 @@ +import { + array as arraySchema, + ArraySchema as IArraySchema, + boolean as booleanSchema, + number as numberSchema, + object as objectSchema, + ObjectSchema as IObjectSchema, + Schema, + string as stringSchema, + StringSchema as IStringSchema +} from 'yup'; + +import { SliseAdPlacesRule, SliseAdProvidersByDomainRule, StylePropName, stylePropsNames } from '../advertising/slise'; +import { isValidSelectorsGroup } from '../utils/selectors.min.js'; +import { isDefined } from './helpers'; + +type nullish = null | undefined; + +const makeDictionarySchema = (keySchema: IStringSchema, valueSchema: Schema) => + objectSchema() + .test('keys-are-valid', async (value: object | nullish) => { + if (!isDefined(value)) { + return true; + } + + await Promise.all(Object.keys(value).map(key => keySchema.validate(key))); + + return true; + }) + .test('values-are-valid', async (value: object | nullish) => { + if (!isDefined(value)) { + return true; + } + + await Promise.all(Object.values(value).map(value => valueSchema.validate(value))); + + return true; + }) as IObjectSchema>; + +const regexStringSchema = stringSchema().test('is-regex', function (value: string | undefined) { + try { + if (!isDefined(value)) { + throw new Error(); + } + + new RegExp(value); + + return true; + } catch (e) { + throw this.createError({ path: this.path, message: `${value} must be a valid regex string` }); + } +}); + +export const regexStringListSchema = arraySchema().of(regexStringSchema.clone().required()); + +const cssSelectorSchema = stringSchema().test('is-css-selector', function (value: string | undefined) { + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + if (isDefined(value) && isValidSelectorsGroup(value)) { + return true; + } + + throw this.createError({ path: this.path, message: `${value} must be a valid CSS selector` }); +}); + +const cssSelectorsListSchema = arraySchema().of(cssSelectorSchema.clone().required()); + +const hostnameSchema = stringSchema().matches( + /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)+([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$/, + params => `${params.value} is an invalid hostname` +); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const hostnamesListSchema: IArraySchema = arraySchema() + .of(hostnameSchema.clone().required()) + .required(); + +const adTypeSchema = stringSchema().min(1); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const adTypesListSchema: IArraySchema = arraySchema() + .of(adTypeSchema.clone().required()) + .min(1) + .required() + .typeError('Must be a non-empty string'); + +const styleSchema: IObjectSchema> = makeDictionarySchema( + stringSchema() + .oneOf(stylePropsNames, params => `${params.value} is an unknown style property`) + .required(), + stringSchema().required() +); + +const sliseAdStylesOverridesSchema = objectSchema().shape({ + parentDepth: numberSchema().integer().min(0).required(), + style: styleSchema.required() +}); + +const sliseAdPlacesRulesSchema = arraySchema() + .of( + objectSchema() + .shape({ + urlRegexes: arraySchema().of(regexStringSchema.clone().required()).required(), + selector: objectSchema() + .shape({ + isMultiple: booleanSchema().required(), + cssString: cssSelectorSchema.clone().required(), + parentDepth: numberSchema().integer().min(0).required(), + shouldUseDivWrapper: booleanSchema().required(), + divWrapperStyle: styleSchema + }) + .required(), + stylesOverrides: arraySchema().of(sliseAdStylesOverridesSchema.clone().required()) + }) + .required() + ) + .required(); + +export const sliseAdPlacesRulesDictionarySchema: IObjectSchema> = + makeDictionarySchema(hostnameSchema, sliseAdPlacesRulesSchema).required(); + +const sliseAdProvidersByDomainRulesSchema = arraySchema() + .of( + objectSchema() + .shape({ + urlRegexes: arraySchema().of(regexStringSchema.clone().required()).required(), + providers: arraySchema().of(stringSchema().required()).required() + }) + .required() + ) + .required(); + +export const sliseAdProvidersByDomainsRulesDictionarySchema: IObjectSchema< + Record +> = makeDictionarySchema(hostnameSchema, sliseAdProvidersByDomainRulesSchema).required(); + +export const sliseAdProvidersDictionarySchema: IObjectSchema> = makeDictionarySchema( + adTypeSchema.clone().required(), + cssSelectorsListSchema.clone().required() +).required(); diff --git a/src/utils/selectors.min.js b/src/utils/selectors.min.js new file mode 100644 index 0000000..12f6ca4 --- /dev/null +++ b/src/utils/selectors.min.js @@ -0,0 +1,4 @@ +/* eslint-disable */ +/*! Selectors.js v1.0.59 | (c) https://github.com/selectors/selectors.js | https://github.com/selectors/selectors.js/blob/master/LICENSE.md */ +"use strict";var s={};s.isValidSelectorsGroup=function(a){if("string"!=typeof a)throw new Error("s.isValidSelectorsGroup expected string value, instead was passed: "+a);return""===a?!1:s._isExactMatch(s._selectors_group,a)},s.isValidSelector=function(a,b){if("string"!=typeof a)throw new Error("s.isValidSelector expected string value as its first argument, instead was passed: "+selectorsGroup);var b="function"==typeof s._isValidHtml&&b||!1;if("boolean"!=typeof b)throw new Error("s.isValidSelector expected boolean value as its second argument, instead was passed: "+selectorsGroup);try{switch(s.getType(a).type){case"type":if(b)return s._isValidHtml("type",a);case"attribute":if(b)return s._isValidHtml("attribute",a);case"universal":case"class":case"id":case"negation":return!0;case"pseudo-class":return s._isValidCssPseudoClass(a);case"pseudo-element":return s._isValidCssPseudoElement(a)}}catch(c){return!1}},s.quickValidation=function(a){if(!document.querySelector)throw new Error("This browser does not support `document.querySelector` which is used by `s.quickValidation`.");try{return document.querySelector(a),!0}catch(b){return!1}},s.getType=function(a){if(!a||"string"!=typeof a)throw new Error("s.getType should be passed a non-empty string value, instead was passed "+a);var b,c;if(s._isExactMatch(s._combinator,a))c="combinator";else if(s._isExactMatch(s._type_selector,a))b=s._splitNamespaceAndName(a),c="type";else if(s._isExactMatch(s._universal,a))b=s._splitNamespaceAndName(a),c="universal";else if(s._isExactMatch(s._class,a))c="class";else if(s._isExactMatch(s._HASH,a))c="id";else if(s._isExactMatch(s._attrib,a))c="attribute";else if(s._isExactMatch(s._negation,a))c="negation";else{if(!s._isExactMatch(s._pseudo,a))throw new Error("s.getType should be passed 1 valid selector, instead was passed: "+a);c=":"!==a.charAt(1)&&":first-line"!==a&&":first-letter"!==a&&":before"!==a&&":after"!==a?"pseudo-class":"pseudo-element"}return b?{namespace:b.namespace,type:c}:{type:c}},s.getSequences=function(a){if(!a||"string"!=typeof a)return[];var b=[],c=a.split(",");return c.forEach(function(a){b.push(a.trim())}),b},s.getSelectors=function(a){if(!a||"string"!=typeof a)return[];s._r.getSelectors||(s._r.getSelectors=new RegExp(s._negation+"|("+s._namespace_prefix+"?("+s._type_selector+"|"+s._universal+"))|"+s._HASH+"|"+s._class+"|"+s._attrib+"|::?("+s._functional_pseudo+"|"+s._ident+")|"+s._combinator,"g"));var b=[];a.replace(s._r.getSelectors,function(a){if(a){var c=a.trim();b.push(""==c&&a.length>0?" ":c)}return""});return b},s.getElements=function(a){if(!a||"string"!=typeof a)return[];var b=[],c=-1;return s.getSelectors(a).forEach(function(a,d){0!==d&&"combinator"!==s.getType(a).type||(c=b.push([])-1),b[c].push(a)}),b},s.getAttributeProperties=function(a){if(!a||"string"!=typeof a)return!1;if("attribute"!==s.getType(a).type)throw new Error("s.getAttributeProperties should be passed 1 valid attribute selector, instead was passed "+a);var b,c={namespace:null,name:null,symbol:null,value:null};if(b=s._getNamespaceAndNameFromAttributeSelector(a),b.indexOf("|")>-1){var d=s._splitNamespaceAndName(b);c.namespace=d.namespace,c.name=d.name}else c.name=b;return c.symbol=s._getSymbolFromAttributeSelector(a),c.value=s._getValueFromAttributeSelector(a),c},s.getPseudoProperties=function(a){if(!a||"string"!=typeof a)return!1;var b=s.getType(a).type;if("pseudo-class"!==b&&"pseudo-element"!==b)throw new Error("s.getPseudoProperties should be passed 1 valid pseudo-class or pseudo-element selector, instead was passed "+a);var c={vendor:s._getVendorPrefixFromPseudoSelector(a),name:s._getNameFromPseudoSelector(a),args:s._getArgsFromPseudoClass(a)};return":first-line"===a||":first-letter"===a||":before"===a||":after"===a?c.colons=1:"::first-line"!==a&&"::first-letter"!==a&&"::before"!==a&&"::after"!==a||(c.colons=2),c},s.getNegationInnerSelectorProperties=function(a){if(!a||"string"!=typeof a)return!1;var b=s.getType(a).type;if("negation"!==b)throw new Error("s.getNegationInnerSelectorProperties should be passed 1 valid negation selector, instead was passed "+pseudoSelector);var c=s._getArgsFromPseudoClass(a),d={selector:c,type:s.getType(c).type};if("negation"===d.type||"pseudo-element"===d.type)throw new Error("s.getNegationInnerSelectorProperties was passed a negation selector containing a "+d.type+" selector. Negation selectors are not allowed to contain other negation selectors or pseudo-element selectors.");return d},s.stripNoise=function(a){return a&&"string"==typeof a?(s._r.stipNoise||(s._r.stripNoise=new RegExp("\\s*({.*$|"+s._comment+"|"+s._badcomment+")","gm")),s._r.newLines||(s._r.newLines=new RegExp(s._nl,"gm")),a.replace(s._r.newLines,"").replace(s._r.stripNoise,function(a){return""})):[]},s._r={},s._isExactMatch=function(a,b){return a instanceof RegExp&&(a=a.source),s._r[a]||(s._r[a]=new RegExp("^"+a+"$")),s._r[a].test(b)},s._h="[0-9a-fA-F]",s._nonascii="(?![\\u0000-\\u0239]).*",s._unicode="(\\\\"+s._h+"{1,6}(\\r\\n|[ \\t\\r\\n\\f])?)",s._escape="("+s._unicode+"|\\\\[^\\r\\n\\f0-9a-f])",s._nmstart="([_a-zA-Z]|"+s._nonascii+"|"+s._escape+")",s._nmchar="([_a-zA-Z0-9-]|"+s._nonascii+"|"+s._escape+")",s._ident="(-?"+s._nmstart+s._nmchar+"*)",s._name=s._nmchar+"+",s._num="([0-9]+|[0-9]*\\.[0-9]+)",s._s="[ \\t\\r\\n\\f]+",s._w="[ \\t\\r\\n\\f]*",s._nl="\\n|\\r\\n|\\r|\\f",s._string1='(\\"([^\\n\\r\\f\\"]|\\'+s._nl+"|"+s._nonascii+"|"+s._escape+')*\\")',s._string2="(\\'([^\\n\\r\\f\\']|\\"+s._nl+"|"+s._nonascii+"|"+s._escape+")*\\')",s._string="("+s._string1+"|"+s._string2+")",s._badcomment1="\\/\\*[^*]*\\*+([^/*][^*]*\\*+)*",s._badcomment2="\\/\\*[^*]*(\\*+[^/*][^*]*)*",s._badcomment="("+s._badcomment1+"|"+s._badcomment2+")",s._comment="\\/\\*[^*]*\\*+([^/*][^*]*\\*+)*\\/",s._D="([dD]|\\0{0,4}(44|64)(\\r\\n|[ \\t\\r\\n\\f])?)",s._E="([eE]|\\0{0,4}(45|65)(\\r\\n|[ \\t\\r\\n\\f])?)",s._N="([nN]|\\0{0,4}(4e|6e)(\\r\\n|[ \\t\\r\\n\\f])?|\\\\[nN])",s._O="([oO]|\\0{0,4}(4f|6f)(\\r\\n|[ \\t\\r\\n\\f])?|\\\\[oO])",s._T="([tT]|\\0{0,4}(54|74)(\\r\\n|[ \\t\\r\\n\\f])?|\\\\[tT])",s._V="([vV]|\\0{0,4}(58|78)(\\r\\n|[ \\t\\r\\n\\f])?|\\\\[vV])",s._INCLUDES="~=",s._DASHMATCH="\\|=",s._PREFIXMATCH="\\^=",s._SUFFIXMATCH="\\$=",s._SUBSTRINGMATCH="\\*=",s._FUNCTION=s._ident+"\\(",s._HASH="#"+s._name,s._PLUS=s._w+"\\+",s._GREATER=s._w+">",s._COMMA=s._w+",",s._TILDE=s._w+"~",s._NOT=":"+s._N+s._O+s._T+"\\(",s._DIMENSION=s._num+s._ident,s._INTEGER="[0-9]+",s._nth="\\s*(([-+]?("+s._INTEGER+")?"+s._N+"(\\s*[-+]?\\s*"+s._INTEGER+")?|[-+]?"+s._INTEGER+"|"+s._O+s._D+s._D+"|"+s._E+s._V+s._E+s._N+")\\s*)",s._lang=":lang\\("+s._ident+"\\)",s._vendor_prefixed_pseudo="::?[-_]"+s._nmstart+s._nmchar+"*-"+s._nmstart+s._nmchar+"*",s._combinator="(("+s._PLUS+"|"+s._GREATER+"|"+s._TILDE+")\\s*|\\s+)",s._namespace_prefix="("+s._ident+"|\\*)?\\|",s._type_selector="("+s._namespace_prefix+")?"+s._ident,s._universal="("+s._namespace_prefix+")?\\*",s._class="\\."+s._ident,s._attrib="\\[\\s*("+s._namespace_prefix+")?"+s._ident+"\\s*(("+s._PREFIXMATCH+"|"+s._SUFFIXMATCH+"|"+s._SUBSTRINGMATCH+"|=|"+s._INCLUDES+"|"+s._DASHMATCH+")\\s*("+s._ident+"|"+s._string+")\\s*)?\\]",s._expression="(("+s._PLUS+"|-|"+s._DIMENSION+"|"+s._num+"|"+s._string+"|"+s._ident+")\\s*)+",s._functional_pseudo=s._FUNCTION+"\\s*"+s._expression+"\\)",s._pseudo="::?("+s._ident+"|"+s._functional_pseudo+")",s._negation_arg="("+s._type_selector+"|"+s._universal+"|"+s._HASH+"|"+s._class+"|"+s._attrib+"|"+s._pseudo+")",s._negation="("+s._NOT+"\\s*"+s._negation_arg+"\\s*\\))",s._simple_selector_sequence="(("+s._type_selector+"|"+s._universal+")("+s._HASH+"|"+s._class+"|"+s._attrib+"|"+s._pseudo+"|"+s._negation+")*|("+s._HASH+"|"+s._class+"|"+s._attrib+"|"+s._pseudo+"|"+s._negation+")+)",s._selector=s._simple_selector_sequence+"("+s._combinator+s._simple_selector_sequence+")*",s._selectors_group=s._selector+"("+s._COMMA+"\\s*"+s._selector+")*",s._splitNamespaceAndName=function(a){if(!a||"string"!=typeof a)return!1;var b={namespace:null,name:null};return s._r.namespaceAndName||(s._r.namespaceAndName=new RegExp("^"+s._namespace_prefix)),b.name=a.replace(s._r.namespaceAndName,function(a){return b.namespace=a.substr(0,a.length-1),""}),b},s._getNamespaceAndNameFromAttributeSelector=function(a){return a&&"string"==typeof a?(s._r.attributeNamespaceAndName||(s._r.attributeNamespaceAndName=new RegExp("(^\\[\\s*|\\s*(("+s._PREFIXMATCH+"|"+s._SUFFIXMATCH+"|"+s._SUBSTRINGMATCH+"|=|"+s._INCLUDES+"|"+s._DASHMATCH+")\\s*("+s._ident+"|"+s._string+")\\s*)?\\]$)","g")),a.replace(s._r.attributeNamespaceAndName,"")):!1},s._getSymbolFromAttributeSelector=function(a){return a&&"string"==typeof a?(s._r.attributeSymbol||(s._r.attributeSymbol=new RegExp("(^\\[\\s*("+s._namespace_prefix+")?"+s._ident+"\\s*|\\s*("+s._ident+"|"+s._string+")\\s*|\\]$)","g")),a.replace(s._r.attributeSymbol,"")):!1},s._getValueFromAttributeSelector=function(a){return a&&"string"==typeof a?(s._r.attributeValue||(s._r.attributeValue=new RegExp("(^\\[\\s*("+s._namespace_prefix+")?"+s._ident+"\\s*("+s._PREFIXMATCH+"|"+s._SUFFIXMATCH+"|"+s._SUBSTRINGMATCH+"|=|"+s._INCLUDES+"|"+s._DASHMATCH+")\\s*[\"']?|[\"']?\\s*\\]$)","g")),a.replace(s._r.attributeValue,"")):!1},s._isValidCssPseudoClass=function(a){if(!a||"string"!=typeof a)return!1;var b,c=[":root",":first-child",":last-child",":first-of-type",":last-of-type",":only-child",":only-of-type",":empty",":link",":visited",":active",":hover",":focus",":target",":enabled",":disabled",":checked"],d=[":nth-child",":nth-last-child",":nth-of-type",":nth-last-of-type"],e=/\(.*\)$/,f=!1;if(c.indexOf(a.toLowerCase())>-1)return!0;if(b=a.replace(e,function(){return f=!0,""}),f){if(":lang"===b)return s._isExactMatch(s._lang,a);if(d.indexOf(b)>-1){var g=a.match(e,"");return g&&g.length&&g[0]?s._isExactMatch(s._nth,g[0].replace(/\(|\)/g,"")):!1}}return s._isExactMatch(s._vendor_prefixed_pseudo,a)},s._isValidCssPseudoElement=function(a){if(!a||"string"!=typeof a)return!1;switch(a.toLowerCase()){case":first-line":case":first-letter":case":before":case":after":case"::first-line":case"::first-letter":case"::before":case"::after":return!0}return s._isExactMatch(s._vendor_prefixed_pseudo,a)},s._getVendorPrefixFromPseudoSelector=function(a){if(!a||"string"!=typeof a)return!1;if(!s._isExactMatch(s._vendor_prefixed_pseudo,a))return null;s._r.vendorPrefix||(s._r.vendorPrefix=new RegExp(s._nmchar+"-"));var b=a.split(s._r.vendorPrefix);return b[0].substr(":"===a.charAt(1)?2:1,b[0].length)+b[1]+"-"},s._getNameFromPseudoSelector=function(a){return a&&"string"==typeof a?(s._r.pseudoName||(s._r.pseudoName=new RegExp("^::?[-_]"+s._nmstart+s._nmchar+"*-|^::?|\\(.*\\)$","g")),a.replace(s._r.pseudoName,"")):!1},s._getArgsFromPseudoClass=function(a){return a&&"string"==typeof a?s._isValidCssPseudoElement(a)||!/\)$/.test(a)?null:a.replace(/^:.*\(|\)$/g,""):!1}; +module.exports = s; diff --git a/yarn.lock b/yarn.lock index 47b0342..12090f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,38 @@ # yarn lockfile v1 +"@apidevtools/json-schema-ref-parser@^9.0.6": + version "9.1.2" + resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz#8ff5386b365d4c9faa7c8b566ff16a46a577d9b8" + integrity sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg== + dependencies: + "@jsdevtools/ono" "^7.1.3" + "@types/json-schema" "^7.0.6" + call-me-maybe "^1.0.1" + js-yaml "^4.1.0" + +"@apidevtools/openapi-schemas@^2.0.4": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz#9fa08017fb59d80538812f03fc7cac5992caaa17" + integrity sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ== + +"@apidevtools/swagger-methods@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz#b789a362e055b0340d04712eafe7027ddc1ac267" + integrity sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg== + +"@apidevtools/swagger-parser@10.0.3": + version "10.0.3" + resolved "https://registry.yarnpkg.com/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz#32057ae99487872c4dd96b314a1ab4b95d89eaf5" + integrity sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g== + dependencies: + "@apidevtools/json-schema-ref-parser" "^9.0.6" + "@apidevtools/openapi-schemas" "^2.0.4" + "@apidevtools/swagger-methods" "^3.0.2" + "@jsdevtools/ono" "^7.1.3" + call-me-maybe "^1.0.1" + z-schema "^5.0.1" + "@babel/code-frame@7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" @@ -241,6 +273,11 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" +"@jsdevtools/ono@^7.1.3": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" + integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -589,6 +626,11 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/json-schema@^7.0.6": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + "@types/json-schema@^7.0.9": version "7.0.11" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" @@ -901,6 +943,11 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + args@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/args/-/args-5.0.1.tgz#4bf298df90a4799a09521362c579278cc2fdd761" @@ -1134,6 +1181,11 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" +call-me-maybe@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.2.tgz#03f964f19522ba643b1b0693acb9152fe2074baa" + integrity sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ== + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -1229,6 +1281,16 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +commander@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.0.tgz#b990bfb8ac030aedc6d11bc04d1488ffef56db75" + integrity sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q== + +commander@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" + integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== + compressible@^2.0.12: version "2.0.18" resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" @@ -1439,6 +1501,13 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" +doctrine@3.0.0, doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -1446,13 +1515,6 @@ doctrine@^2.1.0: dependencies: esutils "^2.0.2" -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - dot-prop@^5.2.0: version "5.3.0" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" @@ -2204,6 +2266,18 @@ glob-parent@^5.1.2, glob-parent@~5.1.0: dependencies: is-glob "^4.0.1" +glob@7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + glob@^7.1.3: version "7.1.7" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" @@ -2744,6 +2818,13 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + json-bigint@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" @@ -2892,6 +2973,11 @@ lodash.defaults@^4.2.0: resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== + lodash.includes@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" @@ -2907,6 +2993,11 @@ lodash.isboolean@^3.0.3: resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + lodash.isinteger@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" @@ -2932,6 +3023,11 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== +lodash.mergewith@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" + integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== + lodash.once@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" @@ -3438,6 +3534,11 @@ progress@^2.0.0: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== +property-expr@^2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.6.tgz#f77bc00d5928a6c748414ad12882e83f24aec1e8" + integrity sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA== + proto3-json-serializer@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/proto3-json-serializer/-/proto3-json-serializer-0.1.8.tgz#f80f9afc1efe5ed9a9856bbbd17dc7cabd7ce9a3" @@ -4031,6 +4132,37 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +swagger-jsdoc@^6.2.8: + version "6.2.8" + resolved "https://registry.yarnpkg.com/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz#6d33d9fb07ff4a7c1564379c52c08989ec7d0256" + integrity sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ== + dependencies: + commander "6.2.0" + doctrine "3.0.0" + glob "7.1.6" + lodash.mergewith "^4.6.2" + swagger-parser "^10.0.3" + yaml "2.0.0-1" + +swagger-parser@^10.0.3: + version "10.0.3" + resolved "https://registry.yarnpkg.com/swagger-parser/-/swagger-parser-10.0.3.tgz#04cb01c18c3ac192b41161c77f81e79309135d03" + integrity sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg== + dependencies: + "@apidevtools/swagger-parser" "10.0.3" + +swagger-ui-dist@>=5.0.0: + version "5.10.3" + resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-5.10.3.tgz#903adbfbecc0670a802b6d8b770e5dd07b5a36cb" + integrity sha512-fu3aozjxFWsmcO1vyt1q1Ji2kN7KlTd1vHy27E9WgPyXo9nrEzhQPqgxaAjbMsOmb8XFKNGo4Sa3Q+84Fh+pFw== + +swagger-ui-express@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/swagger-ui-express/-/swagger-ui-express-5.0.0.tgz#7a00a18dd909574cb0d628574a299b9ba53d4d49" + integrity sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA== + dependencies: + swagger-ui-dist ">=5.0.0" + table@^6.0.9: version "6.8.1" resolved "https://registry.yarnpkg.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf" @@ -4066,6 +4198,11 @@ timers-ext@^0.1.7: es5-ext "~0.10.46" next-tick "1" +tiny-case@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03" + integrity sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q== + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -4078,6 +4215,11 @@ toidentifier@1.0.1: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== +toposort@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" + integrity sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg== + tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -4172,6 +4314,11 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== +type-fest@^2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== + type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -4288,6 +4435,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +validator@^13.7.0: + version "13.11.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.11.0.tgz#23ab3fd59290c61248364eabf4067f04955fbb1b" + integrity sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ== + vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" @@ -4411,6 +4563,11 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yaml@2.0.0-1: + version "2.0.0-1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.0.0-1.tgz#8c3029b3ee2028306d5bcf396980623115ff8d18" + integrity sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ== + yargs-parser@^20.2.2: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" @@ -4438,3 +4595,24 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +yup@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/yup/-/yup-1.3.2.tgz#afffc458f1513ed386e6aaf4bcaa4e67a9e270dc" + integrity sha512-6KCM971iQtJ+/KUaHdrhVr2LDkfhBtFPRnsG1P8F4q3uUVQ2RfEM9xekpha9aA4GXWJevjM10eDcPQ1FfWlmaQ== + dependencies: + property-expr "^2.0.5" + tiny-case "^1.0.3" + toposort "^2.0.2" + type-fest "^2.19.0" + +z-schema@^5.0.1: + version "5.0.6" + resolved "https://registry.yarnpkg.com/z-schema/-/z-schema-5.0.6.tgz#46d6a687b15e4a4369e18d6cb1c7b8618fc256c5" + integrity sha512-+XR1GhnWklYdfr8YaZv/iu+vY+ux7V5DS5zH1DQf6bO5ufrt/5cgNhVO5qyhsjFXvsqQb/f08DWE9b6uPscyAg== + dependencies: + lodash.get "^4.4.2" + lodash.isequal "^4.5.0" + validator "^13.7.0" + optionalDependencies: + commander "^10.0.0"