From 51fc09bb206d4c5451b269b68003a7e976858d06 Mon Sep 17 00:00:00 2001 From: Inokentii Mazhara Date: Mon, 11 Dec 2023 19:13:54 +0200 Subject: [PATCH 01/14] TW-1192 Implement a backend for ads replacement management --- .env.dist | 2 + package.json | 5 +- src/advertising/slise.ts | 70 ++++ src/config.ts | 10 +- src/index.ts | 22 +- src/middlewares/basic-auth.middleware.ts | 21 +- src/routers/slise-rules-router.ts | 500 +++++++++++++++++++++++ yarn.lock | 202 ++++++++- 8 files changed, 816 insertions(+), 16 deletions(-) create mode 100644 src/advertising/slise.ts create mode 100644 src/routers/slise-rules-router.ts diff --git a/.env.dist b/.env.dist index 2b50f2b..57b8115 100644 --- a/.env.dist +++ b/.env.dist @@ -10,3 +10,5 @@ THREE_ROUTE_API_AUTH_TOKEN= REDIS_URL= ADD_NOTIFICATION_USERNAME= ADD_NOTIFICATION_PASSWORD= +MANAGE_ADS_USERNAME= +MANAGE_ADS_PASSWORD= diff --git a/package.json b/package.json index 3d580b2..5b56ca3 100644 --- a/package.json +++ b/package.json @@ -19,13 +19,16 @@ "express": "^4.18.2", "firebase-admin": "^10.0.2", "ioredis": "^5.3.2", + "joi": "^17.11.0", "lodash": "^4.17.21", "memoizee": "^0.4.15", "pino": "^6.11.2", "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" }, "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..e69504d --- /dev/null +++ b/src/advertising/slise.ts @@ -0,0 +1,70 @@ +import { redisClient } from '../redis'; +import { isDefined } from '../utils/helpers'; + +export interface SliseAdContainerRule { + urlRegexes: string[]; + selector: { + isMultiple: boolean; + cssString: string; + shouldUseResultParent: boolean; + }; +} + +const SLISE_AD_CONTAINERS_RULES_KEY = 'slise_ad_containers_rules'; +const SLISE_HEURISTIC_URL_REGEXES_KEY = 'slise_heuristic_url_regexes_key'; +const SLISE_HEURISTIC_SELECTORS_KEY = 'slise_heuristic_selectors_key'; + +export const getSliseAdContainerRulesByDomain = async (domain: string) => { + const rule = await redisClient.hget(SLISE_AD_CONTAINERS_RULES_KEY, domain); + + return isDefined(rule) + ? (JSON.parse(rule) as SliseAdContainerRule[]) + : [ + { + urlRegexes: [], + selector: { + isMultiple: false, + cssString: '*', + shouldUseResultParent: false + } + } + ]; +}; + +export const getAllSliseAdContainerRules = async () => { + const rules = await redisClient.hgetall(SLISE_AD_CONTAINERS_RULES_KEY); + + const parsedRules: { [domain: string]: SliseAdContainerRule[] } = {}; + for (const domainName in rules) { + parsedRules[domainName] = JSON.parse(rules[domainName]); + } + + return parsedRules; +}; + +export const upsertSliseAdContainerRules = async (rules: Record) => { + // Failed to set the arrays as values in Redis, so stringifying them + await redisClient.hmset( + SLISE_AD_CONTAINERS_RULES_KEY, + Object.fromEntries(Object.entries(rules).map(([domain, rules]) => [domain, JSON.stringify(rules)])) + ); +}; + +export const removeSliseAdContainerRules = async (domains: string[]) => + redisClient.hdel(SLISE_AD_CONTAINERS_RULES_KEY, ...domains); + +export const getSliseHeuristicUrlRegexes = async () => redisClient.smembers(SLISE_HEURISTIC_URL_REGEXES_KEY); + +export const addSliseHeuristicUrlRegexes = async (regexes: string[]) => + redisClient.sadd(SLISE_HEURISTIC_URL_REGEXES_KEY, ...regexes); + +export const removeSliseHeuristicUrlRegexes = async (regexes: string[]) => + redisClient.srem(SLISE_HEURISTIC_URL_REGEXES_KEY, ...regexes); + +export const getSliseHeuristicSelectors = async () => redisClient.smembers(SLISE_HEURISTIC_SELECTORS_KEY); + +export const addSliseHeuristicSelectors = async (selectors: string[]) => + redisClient.sadd(SLISE_HEURISTIC_SELECTORS_KEY, ...selectors); + +export const removeSliseHeuristicSelectors = async (selectors: string[]) => + redisClient.srem(SLISE_HEURISTIC_SELECTORS_KEY, ...selectors); diff --git a/src/config.ts b/src/config.ts index 4cfd913..a0f7f21 100644 --- a/src/config.ts +++ b/src/config.ts @@ -5,14 +5,16 @@ export const MIN_IOS_APP_VERSION = '1.10.445'; export const MIN_ANDROID_APP_VERSION = '1.10.445'; export const EnvVars = { - MOONPAY_SECRET_KEY: getEnv('MOONPAY_SECRET_KEY'), - ALICE_BOB_PRIVATE_KEY: getEnv('ALICE_BOB_PRIVATE_KEY'), - ALICE_BOB_PUBLIC_KEY: getEnv('ALICE_BOB_PUBLIC_KEY'), + MOONPAY_SECRET_KEY: '', // getEnv('MOONPAY_SECRET_KEY'), + ALICE_BOB_PRIVATE_KEY: '', // getEnv('ALICE_BOB_PRIVATE_KEY'), + ALICE_BOB_PUBLIC_KEY: '', // getEnv('ALICE_BOB_PUBLIC_KEY'), 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') + ADD_NOTIFICATION_PASSWORD: getEnv('ADD_NOTIFICATION_PASSWORD'), + MANAGE_ADS_USERNAME: getEnv('MANAGE_ADS_USERNAME'), + MANAGE_ADS_PASSWORD: getEnv('MANAGE_ADS_PASSWORD') }; for (const name in EnvVars) { diff --git a/src/index.ts b/src/index.ts index 1a87be7..030074e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,17 +6,20 @@ 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'; import getDAppsStats from './getDAppsStats'; -import { basicAuth } from './middlewares/basic-auth.middleware'; +import { basicAuth, BasicAuthRights } from './middlewares/basic-auth.middleware'; import { Notification, PlatformType } from './notifications/notification.interface'; import { getImageFallback } from './notifications/utils/get-image-fallback.util'; 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-rules-router'; 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'; @@ -115,7 +118,7 @@ app.get('/api/notifications', async (_req, res) => { } }); -app.post('/api/notifications', basicAuth, async (req, res) => { +app.post('/api/notifications', basicAuth(BasicAuthRights.AddNotification), async (req, res) => { try { const { mobile, @@ -322,6 +325,21 @@ app.get('/api/advertising-info', (_req, res) => { } }); +app.use('/api/slise-ad-container-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..28eb93a 100644 --- a/src/middlewares/basic-auth.middleware.ts +++ b/src/middlewares/basic-auth.middleware.ts @@ -3,13 +3,30 @@ import { Request, Response, NextFunction } from 'express'; import { EnvVars } from '../config'; import { isDefined } from '../utils/helpers'; -export const basicAuth = (req: Request, res: Response, next: NextFunction) => { +export enum BasicAuthRights { + AddNotification = 'add-notification', + ManageAds = 'manage-ads' +} + +const credentials = { + [BasicAuthRights.AddNotification]: { + username: EnvVars.ADD_NOTIFICATION_USERNAME, + password: EnvVars.ADD_NOTIFICATION_PASSWORD + }, + [BasicAuthRights.ManageAds]: { + username: EnvVars.MANAGE_ADS_USERNAME, + password: EnvVars.MANAGE_ADS_PASSWORD + } +}; + +export const basicAuth = (rights: BasicAuthRights) => (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[rights]; - 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-rules-router.ts b/src/routers/slise-rules-router.ts new file mode 100644 index 0000000..7fbece1 --- /dev/null +++ b/src/routers/slise-rules-router.ts @@ -0,0 +1,500 @@ +import { NextFunction, Request, RequestHandler, Response, Router } from 'express'; +import Joi, { Schema, ArraySchema, ObjectSchema } from 'joi'; + +import { + addSliseHeuristicSelectors, + addSliseHeuristicUrlRegexes, + getAllSliseAdContainerRules, + getSliseAdContainerRulesByDomain, + getSliseHeuristicSelectors, + getSliseHeuristicUrlRegexes, + removeSliseAdContainerRules, + removeSliseHeuristicSelectors, + removeSliseHeuristicUrlRegexes, + SliseAdContainerRule, + upsertSliseAdContainerRules +} from '../advertising/slise'; +import { basicAuth, BasicAuthRights } from '../middlewares/basic-auth.middleware'; + +type TypedBodyRequestHandler = ( + req: Request, unknown, T>, + res: Response, + next: NextFunction +) => void; + +const withBodyValidation = + (schema: Schema, handler: TypedBodyRequestHandler): RequestHandler => + (req, res, next) => { + const { value, error } = schema.validate(req.body); + + if (error) { + return res.status(400).send({ error: error.message }); + } + + req.body = value; + + return handler(req, res, next); + }; + +const withExceptionHandler = + (handler: RequestHandler): RequestHandler => + async (req, res, next) => { + try { + await handler(req, res, next); + } catch (error) { + res.status(500).send({ error }); + } + }; + +/** + * @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 + * schemas: + * SliseAdContainerSelector: + * type: object + * required: + * - isMultiple + * - cssString + * - shouldUseResultParent + * - shouldUseDivWrapper + * properties: + * isMultiple: + * type: boolean + * description: Whether the selector should return multiple elements + * cssString: + * type: string + * description: CSS selector + * shouldUseResultParent: + * type: boolean + * description: Whether the results parents should be used as ads containers + * shouldUseDivWrapper: + * type: boolean + * description: Whether the ads banner should be wrapped in a div + * SliseAdContainerRule: + * type: object + * required: + * - urlRegexes + * - selector + * properties: + * urlRegexes: + * type: array + * items: + * type: string + * format: regex + * description: List of regexes to match the site URL against + * selector: + * $ref: '#/components/schemas/SliseAdContainerSelector' + * SliseAdContainerRulesDictionary: + * type: object + * additionalProperties: + * type: array + * items: + * $ref: '#/components/schemas/SliseAdContainerRule' + * description: Dictionary of rules for domains + * example: + * goerli.etherscan.io: + * - urlRegexes: + * - '^https://goerli\.etherscan\.io/?$' + * selector: + * isMultiple: false + * cssString: 'main > section div.row > div:nth-child(2) > div' + * shouldUseResultParent: false + * 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' + * shouldUseResultParent: true + * 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)' + * shouldUseResultParent: false + * shouldUseDivWrapper: true + */ + +export const sliseRulesRouter = Router(); + +const sliseAdContainerRulesDictionarySchema: ObjectSchema> = + Joi.object().pattern( + Joi.string().hostname(), + Joi.array() + .items( + Joi.object({ + urlRegexes: Joi.array().items(Joi.string()).required(), + selector: Joi.object({ + isMultiple: Joi.boolean().required(), + cssString: Joi.string().required(), + shouldUseResultParent: Joi.boolean().required(), + shouldUseDivWrapper: Joi.boolean().required() + }).required() + }) + ) + .required() + ); + +const makeStringArraySchema = (errorMessage: string): ArraySchema => + Joi.array() + .items(Joi.string().required()) + .min(1) + .required() + .error(() => errorMessage); + +const sliseHeuristicRulesRouter = Router(); + +/** + * @swagger + * /api/slise-ad-container-rules/heuristic/url-regexes: + * get: + * summary: Get regexes for pages URLs where heuristic search, i.e. by provider, should be used + * responses: + * '200': + * description: List of regexes + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + * format: regex + * '500': + * $ref: '#/components/responses/ErrorResponse' + * post: + * summary: Add regexes for pages URLs where heuristic search, i.e. by provider, should be used + * security: + * - basicAuth: [] + * requestBody: + * description: List of regexes + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + * format: regex + * responses: + * '200': + * $ref: '#/components/responses/SuccessResponse' + * '400': + * $ref: '#/components/responses/ErrorResponse' + * '401': + * $ref: '#/components/responses/UnauthorizedError' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * delete: + * summary: Remove regexes for pages URLs where heuristic search, i.e. by provider, should be used + * security: + * - basicAuth: [] + * requestBody: + * description: List of regexes + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + * format: regex + * responses: + * '200': + * $ref: '#/components/responses/SuccessResponse' + * '400': + * $ref: '#/components/responses/ErrorResponse' + * '401': + * $ref: '#/components/responses/UnauthorizedError' + * '500': + * $ref: '#/components/responses/ErrorResponse' + */ +sliseHeuristicRulesRouter + .route('/url-regexes') + .get( + withExceptionHandler(async (_req, res) => { + const regexes = await getSliseHeuristicUrlRegexes(); + + res.status(200).send(regexes); + }) + ) + .post( + basicAuth(BasicAuthRights.ManageAds), + withExceptionHandler( + withBodyValidation(makeStringArraySchema('The body should be an array of regexp strings'), async (req, res) => { + const regexesAddedCount = await addSliseHeuristicUrlRegexes(req.body); + + res.status(200).send({ message: `${regexesAddedCount} regexes have been added` }); + }) + ) + ) + .delete( + basicAuth(BasicAuthRights.ManageAds), + withExceptionHandler( + withBodyValidation(makeStringArraySchema('The body should be an array of regexp strings'), async (req, res) => { + const regexesRemovedCount = await removeSliseHeuristicUrlRegexes(req.body); + + res.status(200).send({ message: `${regexesRemovedCount} regexes have been removed` }); + }) + ) + ); + +/** + * @swagger + * /api/slise-ad-container-rules/heuristic/selectors: + * get: + * summary: Get CSS selectors for heuristic search + * responses: + * '200': + * description: List of CSS selectors + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + * '500': + * $ref: '#/components/responses/ErrorResponse' + * post: + * summary: Add CSS selectors for heuristic search + * security: + * - basicAuth: [] + * requestBody: + * description: List of CSS selectors + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + * responses: + * '200': + * $ref: '#/components/responses/SuccessResponse' + * '400': + * $ref: '#/components/responses/ErrorResponse' + * '401': + * $ref: '#/components/responses/UnauthorizedError' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * delete: + * summary: Remove CSS selectors for heuristic search + * security: + * - basicAuth: [] + * requestBody: + * description: List of CSS selectors + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + * responses: + * '200': + * $ref: '#/components/responses/SuccessResponse' + * '400': + * $ref: '#/components/responses/ErrorResponse' + * '401': + * $ref: '#/components/responses/UnauthorizedError' + * '500': + * $ref: '#/components/responses/ErrorResponse' + */ +sliseHeuristicRulesRouter + .route('/selectors') + .get( + withExceptionHandler(async (_req, res) => { + const selectors = await getSliseHeuristicSelectors(); + + res.status(200).send(selectors); + }) + ) + .post( + basicAuth(BasicAuthRights.ManageAds), + withExceptionHandler( + withBodyValidation( + makeStringArraySchema('The body should be an array of CSS selector strings'), + async (req, res) => { + const selectorsAddedCount = await addSliseHeuristicSelectors(req.body); + + res.status(200).send({ message: `${selectorsAddedCount} selectors have been added` }); + } + ) + ) + ) + .delete( + basicAuth(BasicAuthRights.ManageAds), + withExceptionHandler( + withBodyValidation( + makeStringArraySchema('The body should be an array of CSS selector strings'), + async (req, res) => { + const selectorsRemovedCount = await removeSliseHeuristicSelectors(req.body); + + res.status(200).send({ message: `${selectorsRemovedCount} selectors have been removed` }); + } + ) + ) + ); + +sliseRulesRouter.use('/heuristic', sliseHeuristicRulesRouter); + +/** + * @swagger + * /api/slise-ad-container-rules/{domain}: + * get: + * summary: Get Slise ad container rule for specified domain + * parameters: + * - in: path + * name: domain + * required: true + * format: hostname + * schema: + * type: string + * responses: + * '200': + * description: Slise ad container rule for the specified domain + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SliseAdContainerRule' + * '500': + * $ref: '#/components/responses/ErrorResponse' + */ +sliseRulesRouter.get('/:domain', async (req, res) => { + try { + const { domain } = req.params; + + const rule = await getSliseAdContainerRulesByDomain(domain); + + res.status(200).send(rule); + } catch (error) { + res.status(500).send({ error }); + } +}); + +/** + * @swagger + * /api/slise-ad-container-rules: + * get: + * summary: Get all Slise ad container rules + * responses: + * '200': + * description: List of Slise ad container rules + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SliseAdContainerRulesDictionary' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * post: + * summary: Upserts Slise ad container rules + * security: + * - basicAuth: [] + * requestBody: + * description: Domain - rules list dictionary of rules + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SliseAdContainerRulesDictionary' + * 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 Slise ad container rules + * security: + * - basicAuth: [] + * requestBody: + * description: List of rule IDs to delete + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + * format: hostname + * responses: + * '200': + * $ref: '#/components/responses/SuccessResponse' + * '400': + * $ref: '#/components/responses/ErrorResponse' + * '401': + * $ref: '#/components/responses/UnauthorizedError' + * '500': + * $ref: '#/components/responses/ErrorResponse' + */ +sliseRulesRouter + .route('/') + .get( + withExceptionHandler(async (_req, res) => { + const rules = await getAllSliseAdContainerRules(); + + res.status(200).send(rules); + }) + ) + .post( + basicAuth(BasicAuthRights.ManageAds), + withExceptionHandler( + withBodyValidation(sliseAdContainerRulesDictionarySchema, async (req, res) => { + const validatedRules = req.body; + + // Adding regex validation to joi is a bit buggy + for (const domain in validatedRules) { + for (const rule of validatedRules[domain]) { + for (const regex of rule.urlRegexes) { + try { + new RegExp(regex); + } catch (error) { + return res.status(400).send({ error: `Invalid regex: ${regex}` }); + } + } + } + } + + await upsertSliseAdContainerRules(validatedRules); + + res.status(200).send({ message: 'Rules have been added successfully' }); + }) + ) + ) + .delete( + basicAuth(BasicAuthRights.ManageAds), + withExceptionHandler( + withBodyValidation(makeStringArraySchema('Domains should be an array of strings'), async (req, res) => { + const removedEntriesCount = await removeSliseAdContainerRules(req.body); + + res.status(200).send({ message: `${removedEntriesCount} entries have been removed` }); + }) + ) + ); diff --git a/yarn.lock b/yarn.lock index 47b0342..3474dfa 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" @@ -204,6 +236,18 @@ resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-2.0.0.tgz#5bb2193eb685c0007540ca61d166d4e1edaf918d" integrity sha512-WEezM1FWztfbzqIUbsDzFRVMxSoLy3HugVcux6KDDtTqzPsLE8NDRHfXvev66aH1i2oOKKar3/XDjbvh/OUBdg== +"@hapi/hoek@^9.0.0": + version "9.3.0" + resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" + integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== + +"@hapi/topo@^5.0.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012" + integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== + dependencies: + "@hapi/hoek" "^9.0.0" + "@humanwhocodes/config-array@^0.5.0": version "0.5.0" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" @@ -241,6 +285,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" @@ -320,6 +369,23 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= +"@sideway/address@^4.1.3": + version "4.1.4" + resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0" + integrity sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw== + dependencies: + "@hapi/hoek" "^9.0.0" + +"@sideway/formula@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" + integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== + +"@sideway/pinpoint@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" + integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== + "@stablelib/binary@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@stablelib/binary/-/binary-1.0.1.tgz#c5900b94368baf00f811da5bdb1610963dfddf7f" @@ -589,6 +655,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 +972,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 +1210,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 +1310,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 +1530,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 +1544,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 +2295,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" @@ -2719,6 +2822,17 @@ jmespath@^0.15.0: resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc= +joi@^17.11.0: + version "17.11.0" + resolved "https://registry.yarnpkg.com/joi/-/joi-17.11.0.tgz#aa9da753578ec7720e6f0ca2c7046996ed04fc1a" + integrity sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ== + dependencies: + "@hapi/hoek" "^9.0.0" + "@hapi/topo" "^5.0.0" + "@sideway/address" "^4.1.3" + "@sideway/formula" "^3.0.1" + "@sideway/pinpoint" "^2.0.0" + jose@^2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/jose/-/jose-2.0.6.tgz#894ba19169af339d3911be933f913dd02fc57c7c" @@ -2744,6 +2858,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 +3013,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 +3033,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 +3063,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" @@ -4031,6 +4167,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" @@ -4288,6 +4455,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 +4583,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 +4615,14 @@ 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== + +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" From 478d35d52a870f27ce734230eaeb32b1e36d997b Mon Sep 17 00:00:00 2001 From: Inokentii Mazhara Date: Tue, 12 Dec 2023 15:49:29 +0200 Subject: [PATCH 02/14] TW-1192 Input validation improvements --- package.json | 3 +- src/advertising/slise.ts | 83 +++-- src/routers/slise-heuristic-rules-router.ts | 211 ++++++++++++ src/routers/slise-rules-router.ts | 349 ++------------------ src/utils/express-helpers.ts | 97 ++++++ src/utils/schemas.ts | 99 ++++++ src/utils/selectors.min.js | 3 + yarn.lock | 30 ++ 8 files changed, 514 insertions(+), 361 deletions(-) create mode 100644 src/routers/slise-heuristic-rules-router.ts create mode 100644 src/utils/express-helpers.ts create mode 100644 src/utils/schemas.ts create mode 100644 src/utils/selectors.min.js diff --git a/package.json b/package.json index 5b56ca3..5f0cf0d 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "qs": "^6.10.3", "semaphore": "^1.1.0", "swagger-jsdoc": "^6.2.8", - "swagger-ui-express": "^5.0.0" + "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 index e69504d..037a721 100644 --- a/src/advertising/slise.ts +++ b/src/advertising/slise.ts @@ -14,44 +14,43 @@ const SLISE_AD_CONTAINERS_RULES_KEY = 'slise_ad_containers_rules'; const SLISE_HEURISTIC_URL_REGEXES_KEY = 'slise_heuristic_url_regexes_key'; const SLISE_HEURISTIC_SELECTORS_KEY = 'slise_heuristic_selectors_key'; -export const getSliseAdContainerRulesByDomain = async (domain: string) => { - const rule = await redisClient.hget(SLISE_AD_CONTAINERS_RULES_KEY, domain); - - return isDefined(rule) - ? (JSON.parse(rule) as SliseAdContainerRule[]) - : [ - { - urlRegexes: [], - selector: { - isMultiple: false, - cssString: '*', - shouldUseResultParent: false - } - } - ]; -}; - -export const getAllSliseAdContainerRules = async () => { - const rules = await redisClient.hgetall(SLISE_AD_CONTAINERS_RULES_KEY); - - const parsedRules: { [domain: string]: SliseAdContainerRule[] } = {}; - for (const domainName in rules) { - parsedRules[domainName] = JSON.parse(rules[domainName]); - } - - return parsedRules; -}; - -export const upsertSliseAdContainerRules = async (rules: Record) => { - // Failed to set the arrays as values in Redis, so stringifying them - await redisClient.hmset( - SLISE_AD_CONTAINERS_RULES_KEY, - Object.fromEntries(Object.entries(rules).map(([domain, rules]) => [domain, JSON.stringify(rules)])) - ); -}; - -export const removeSliseAdContainerRules = async (domains: string[]) => - redisClient.hdel(SLISE_AD_CONTAINERS_RULES_KEY, ...domains); +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: getSliseAdContainerRulesByDomain, + getAllValues: getAllSliseAdContainerRules, + upsertValues: upsertSliseAdContainerRules, + removeValues: removeSliseAdContainerRules +} = objectStorageMethodsFactory(SLISE_AD_CONTAINERS_RULES_KEY, []); + +export const { + getByKey: getSliseHeuristicSelectorsByAdType, + getAllValues: getAllSliseHeuristicSelectors, + upsertValues: upsertSliseHeuristicSelectors, + removeValues: removeSliseHeuristicSelectors +} = objectStorageMethodsFactory(SLISE_HEURISTIC_SELECTORS_KEY, []); export const getSliseHeuristicUrlRegexes = async () => redisClient.smembers(SLISE_HEURISTIC_URL_REGEXES_KEY); @@ -60,11 +59,3 @@ export const addSliseHeuristicUrlRegexes = async (regexes: string[]) => export const removeSliseHeuristicUrlRegexes = async (regexes: string[]) => redisClient.srem(SLISE_HEURISTIC_URL_REGEXES_KEY, ...regexes); - -export const getSliseHeuristicSelectors = async () => redisClient.smembers(SLISE_HEURISTIC_SELECTORS_KEY); - -export const addSliseHeuristicSelectors = async (selectors: string[]) => - redisClient.sadd(SLISE_HEURISTIC_SELECTORS_KEY, ...selectors); - -export const removeSliseHeuristicSelectors = async (selectors: string[]) => - redisClient.srem(SLISE_HEURISTIC_SELECTORS_KEY, ...selectors); diff --git a/src/routers/slise-heuristic-rules-router.ts b/src/routers/slise-heuristic-rules-router.ts new file mode 100644 index 0000000..43a350f --- /dev/null +++ b/src/routers/slise-heuristic-rules-router.ts @@ -0,0 +1,211 @@ +import { Router } from 'express'; + +import { + addSliseHeuristicUrlRegexes, + getAllSliseHeuristicSelectors, + getSliseHeuristicSelectorsByAdType, + getSliseHeuristicUrlRegexes, + removeSliseHeuristicSelectors, + removeSliseHeuristicUrlRegexes, + upsertSliseHeuristicSelectors +} from '../advertising/slise'; +import { basicAuth, BasicAuthRights } from '../middlewares/basic-auth.middleware'; +import { addObjectStorageMethodsToRouter, withBodyValidation, withExceptionHandler } from '../utils/express-helpers'; +import { adTypesListSchema, regexStringListSchema, sliseSelectorsDictionarySchema } from '../utils/schemas'; + +export const sliseHeuristicRulesRouter = Router(); + +/** + * @swagger + * /api/slise-ad-container-rules/heuristic/url-regexes: + * get: + * summary: Get regexes for pages URLs where heuristic search, i.e. by provider, should be used + * responses: + * '200': + * description: List of regexes + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + * format: regex + * example: + * - '^https://www\.dextools\.io/app/[A-z]{2}/[0-9A-z-]+/pair-explorer' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * post: + * summary: Add regexes for pages URLs where heuristic search, i.e. by provider, should be used + * security: + * - basicAuth: [] + * requestBody: + * description: List of regexes + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + * format: regex + * example: + * - '^https://www\.dextools\.io/app/[A-z]{2}/[0-9A-z-]+/pair-explorer' + * responses: + * '200': + * $ref: '#/components/responses/SuccessResponse' + * '400': + * $ref: '#/components/responses/ErrorResponse' + * '401': + * $ref: '#/components/responses/UnauthorizedError' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * delete: + * summary: Remove regexes for pages URLs where heuristic search, i.e. by provider, should be used + * security: + * - basicAuth: [] + * requestBody: + * description: List of regexes + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + * format: regex + * example: + * - '^https://www\.dextools\.io/app/[A-z]{2}/[0-9A-z-]+/pair-explorer' + * responses: + * '200': + * $ref: '#/components/responses/SuccessResponse' + * '400': + * $ref: '#/components/responses/ErrorResponse' + * '401': + * $ref: '#/components/responses/UnauthorizedError' + * '500': + * $ref: '#/components/responses/ErrorResponse' + */ +sliseHeuristicRulesRouter + .route('/url-regexes') + .get( + withExceptionHandler(async (_req, res) => { + const regexes = await getSliseHeuristicUrlRegexes(); + + res.status(200).send(regexes); + }) + ) + .post( + basicAuth(BasicAuthRights.ManageAds), + withExceptionHandler( + withBodyValidation(regexStringListSchema, async (req, res) => { + const regexesAddedCount = await addSliseHeuristicUrlRegexes(req.body); + + res.status(200).send({ message: `${regexesAddedCount} regexes have been added` }); + }) + ) + ) + .delete( + basicAuth(BasicAuthRights.ManageAds), + withExceptionHandler( + withBodyValidation(regexStringListSchema, async (req, res) => { + const regexesRemovedCount = await removeSliseHeuristicUrlRegexes(req.body); + + res.status(200).send({ message: `${regexesRemovedCount} regexes have been removed` }); + }) + ) + ); + +/** + * @swagger + * /api/slise-ad-container-rules/heuristic/selectors/{adType}: + * get: + * summary: Get CSS selectors for heuristic search for specified ad type + * parameters: + * - in: path + * name: adType + * required: true + * type: string + * example: 'coinzilla' + * responses: + * '200': + * description: List of CSS selectors + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + * example: + * - 'iframe[src*="coinzilla.io"]' + * - 'iframe[src*="czilladx.com"]' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * /api/slise-ad-container-rules/heuristic/selectors: + * get: + * summary: Get CSS selectors for heuristic search for all ads types + * responses: + * '200': + * description: Ad type - selectors list dictionary + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SliseAdTypesSelectorsDictionary' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * post: + * summary: Upserts CSS selectors for heuristic search. Selectors for ad types that have existed before will be overwritten + * security: + * - basicAuth: [] + * requestBody: + * description: Ad type - selectors list dictionary + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SliseAdTypesSelectorsDictionary' + * responses: + * '200': + * $ref: '#/components/responses/SuccessResponse' + * '400': + * $ref: '#/components/responses/ErrorResponse' + * '401': + * $ref: '#/components/responses/UnauthorizedError' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * delete: + * summary: Remove CSS selectors for heuristic search + * security: + * - basicAuth: [] + * requestBody: + * description: List of ads types + * 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( + sliseHeuristicRulesRouter, + '/selectors', + { + getByKey: getSliseHeuristicSelectorsByAdType, + getAllValues: getAllSliseHeuristicSelectors, + upsertValues: upsertSliseHeuristicSelectors, + removeValues: removeSliseHeuristicSelectors + }, + 'adType', + sliseSelectorsDictionarySchema, + adTypesListSchema, + removedEntriesCount => `${removedEntriesCount} ad types have been removed`, + BasicAuthRights.ManageAds +); diff --git a/src/routers/slise-rules-router.ts b/src/routers/slise-rules-router.ts index 7fbece1..31d3b27 100644 --- a/src/routers/slise-rules-router.ts +++ b/src/routers/slise-rules-router.ts @@ -1,50 +1,16 @@ -import { NextFunction, Request, RequestHandler, Response, Router } from 'express'; -import Joi, { Schema, ArraySchema, ObjectSchema } from 'joi'; +import { Router } from 'express'; import { - addSliseHeuristicSelectors, - addSliseHeuristicUrlRegexes, getAllSliseAdContainerRules, getSliseAdContainerRulesByDomain, - getSliseHeuristicSelectors, - getSliseHeuristicUrlRegexes, removeSliseAdContainerRules, - removeSliseHeuristicSelectors, - removeSliseHeuristicUrlRegexes, SliseAdContainerRule, upsertSliseAdContainerRules } from '../advertising/slise'; -import { basicAuth, BasicAuthRights } from '../middlewares/basic-auth.middleware'; - -type TypedBodyRequestHandler = ( - req: Request, unknown, T>, - res: Response, - next: NextFunction -) => void; - -const withBodyValidation = - (schema: Schema, handler: TypedBodyRequestHandler): RequestHandler => - (req, res, next) => { - const { value, error } = schema.validate(req.body); - - if (error) { - return res.status(400).send({ error: error.message }); - } - - req.body = value; - - return handler(req, res, next); - }; - -const withExceptionHandler = - (handler: RequestHandler): RequestHandler => - async (req, res, next) => { - try { - await handler(req, res, next); - } catch (error) { - res.status(500).send({ error }); - } - }; +import { BasicAuthRights } from '../middlewares/basic-auth.middleware'; +import { addObjectStorageMethodsToRouter } from '../utils/express-helpers'; +import { hostnamesListSchema, sliseAdContainerRulesDictionarySchema } from '../utils/schemas'; +import { sliseHeuristicRulesRouter } from './slise-heuristic-rules-router'; /** * @swagger @@ -144,224 +110,20 @@ const withExceptionHandler = * cssString: 'div.left-container > app-pe-banner:nth-child(2)' * shouldUseResultParent: false * shouldUseDivWrapper: true + * SliseAdTypesSelectorsDictionary: + * type: object + * additionalProperties: + * type: array + * items: + * type: string + * example: + * coinzilla: + * - 'iframe[src*="coinzilla.io"]' + * - 'iframe[src*="czilladx.com"]' */ export const sliseRulesRouter = Router(); -const sliseAdContainerRulesDictionarySchema: ObjectSchema> = - Joi.object().pattern( - Joi.string().hostname(), - Joi.array() - .items( - Joi.object({ - urlRegexes: Joi.array().items(Joi.string()).required(), - selector: Joi.object({ - isMultiple: Joi.boolean().required(), - cssString: Joi.string().required(), - shouldUseResultParent: Joi.boolean().required(), - shouldUseDivWrapper: Joi.boolean().required() - }).required() - }) - ) - .required() - ); - -const makeStringArraySchema = (errorMessage: string): ArraySchema => - Joi.array() - .items(Joi.string().required()) - .min(1) - .required() - .error(() => errorMessage); - -const sliseHeuristicRulesRouter = Router(); - -/** - * @swagger - * /api/slise-ad-container-rules/heuristic/url-regexes: - * get: - * summary: Get regexes for pages URLs where heuristic search, i.e. by provider, should be used - * responses: - * '200': - * description: List of regexes - * content: - * application/json: - * schema: - * type: array - * items: - * type: string - * format: regex - * '500': - * $ref: '#/components/responses/ErrorResponse' - * post: - * summary: Add regexes for pages URLs where heuristic search, i.e. by provider, should be used - * security: - * - basicAuth: [] - * requestBody: - * description: List of regexes - * content: - * application/json: - * schema: - * type: array - * items: - * type: string - * format: regex - * responses: - * '200': - * $ref: '#/components/responses/SuccessResponse' - * '400': - * $ref: '#/components/responses/ErrorResponse' - * '401': - * $ref: '#/components/responses/UnauthorizedError' - * '500': - * $ref: '#/components/responses/ErrorResponse' - * delete: - * summary: Remove regexes for pages URLs where heuristic search, i.e. by provider, should be used - * security: - * - basicAuth: [] - * requestBody: - * description: List of regexes - * content: - * application/json: - * schema: - * type: array - * items: - * type: string - * format: regex - * responses: - * '200': - * $ref: '#/components/responses/SuccessResponse' - * '400': - * $ref: '#/components/responses/ErrorResponse' - * '401': - * $ref: '#/components/responses/UnauthorizedError' - * '500': - * $ref: '#/components/responses/ErrorResponse' - */ -sliseHeuristicRulesRouter - .route('/url-regexes') - .get( - withExceptionHandler(async (_req, res) => { - const regexes = await getSliseHeuristicUrlRegexes(); - - res.status(200).send(regexes); - }) - ) - .post( - basicAuth(BasicAuthRights.ManageAds), - withExceptionHandler( - withBodyValidation(makeStringArraySchema('The body should be an array of regexp strings'), async (req, res) => { - const regexesAddedCount = await addSliseHeuristicUrlRegexes(req.body); - - res.status(200).send({ message: `${regexesAddedCount} regexes have been added` }); - }) - ) - ) - .delete( - basicAuth(BasicAuthRights.ManageAds), - withExceptionHandler( - withBodyValidation(makeStringArraySchema('The body should be an array of regexp strings'), async (req, res) => { - const regexesRemovedCount = await removeSliseHeuristicUrlRegexes(req.body); - - res.status(200).send({ message: `${regexesRemovedCount} regexes have been removed` }); - }) - ) - ); - -/** - * @swagger - * /api/slise-ad-container-rules/heuristic/selectors: - * get: - * summary: Get CSS selectors for heuristic search - * responses: - * '200': - * description: List of CSS selectors - * content: - * application/json: - * schema: - * type: array - * items: - * type: string - * '500': - * $ref: '#/components/responses/ErrorResponse' - * post: - * summary: Add CSS selectors for heuristic search - * security: - * - basicAuth: [] - * requestBody: - * description: List of CSS selectors - * content: - * application/json: - * schema: - * type: array - * items: - * type: string - * responses: - * '200': - * $ref: '#/components/responses/SuccessResponse' - * '400': - * $ref: '#/components/responses/ErrorResponse' - * '401': - * $ref: '#/components/responses/UnauthorizedError' - * '500': - * $ref: '#/components/responses/ErrorResponse' - * delete: - * summary: Remove CSS selectors for heuristic search - * security: - * - basicAuth: [] - * requestBody: - * description: List of CSS selectors - * content: - * application/json: - * schema: - * type: array - * items: - * type: string - * responses: - * '200': - * $ref: '#/components/responses/SuccessResponse' - * '400': - * $ref: '#/components/responses/ErrorResponse' - * '401': - * $ref: '#/components/responses/UnauthorizedError' - * '500': - * $ref: '#/components/responses/ErrorResponse' - */ -sliseHeuristicRulesRouter - .route('/selectors') - .get( - withExceptionHandler(async (_req, res) => { - const selectors = await getSliseHeuristicSelectors(); - - res.status(200).send(selectors); - }) - ) - .post( - basicAuth(BasicAuthRights.ManageAds), - withExceptionHandler( - withBodyValidation( - makeStringArraySchema('The body should be an array of CSS selector strings'), - async (req, res) => { - const selectorsAddedCount = await addSliseHeuristicSelectors(req.body); - - res.status(200).send({ message: `${selectorsAddedCount} selectors have been added` }); - } - ) - ) - ) - .delete( - basicAuth(BasicAuthRights.ManageAds), - withExceptionHandler( - withBodyValidation( - makeStringArraySchema('The body should be an array of CSS selector strings'), - async (req, res) => { - const selectorsRemovedCount = await removeSliseHeuristicSelectors(req.body); - - res.status(200).send({ message: `${selectorsRemovedCount} selectors have been removed` }); - } - ) - ) - ); - sliseRulesRouter.use('/heuristic', sliseHeuristicRulesRouter); /** @@ -376,30 +138,16 @@ sliseRulesRouter.use('/heuristic', sliseHeuristicRulesRouter); * format: hostname * schema: * type: string + * example: 'goerli.etherscan.io' * responses: * '200': - * description: Slise ad container rule for the specified domain + * description: Slise ad container rules for the specified domain * content: * application/json: * schema: * $ref: '#/components/schemas/SliseAdContainerRule' * '500': * $ref: '#/components/responses/ErrorResponse' - */ -sliseRulesRouter.get('/:domain', async (req, res) => { - try { - const { domain } = req.params; - - const rule = await getSliseAdContainerRulesByDomain(domain); - - res.status(200).send(rule); - } catch (error) { - res.status(500).send({ error }); - } -}); - -/** - * @swagger * /api/slise-ad-container-rules: * get: * summary: Get all Slise ad container rules @@ -413,7 +161,7 @@ sliseRulesRouter.get('/:domain', async (req, res) => { * '500': * $ref: '#/components/responses/ErrorResponse' * post: - * summary: Upserts Slise ad container rules + * summary: Upserts Slise ad container rules. Rules for domains that have existed before will be overwritten * security: * - basicAuth: [] * requestBody: @@ -444,6 +192,8 @@ sliseRulesRouter.get('/:domain', async (req, res) => { * items: * type: string * format: hostname + * example: + * - 'goerli.etherscan.io' * responses: * '200': * $ref: '#/components/responses/SuccessResponse' @@ -454,47 +204,18 @@ sliseRulesRouter.get('/:domain', async (req, res) => { * '500': * $ref: '#/components/responses/ErrorResponse' */ -sliseRulesRouter - .route('/') - .get( - withExceptionHandler(async (_req, res) => { - const rules = await getAllSliseAdContainerRules(); - - res.status(200).send(rules); - }) - ) - .post( - basicAuth(BasicAuthRights.ManageAds), - withExceptionHandler( - withBodyValidation(sliseAdContainerRulesDictionarySchema, async (req, res) => { - const validatedRules = req.body; - - // Adding regex validation to joi is a bit buggy - for (const domain in validatedRules) { - for (const rule of validatedRules[domain]) { - for (const regex of rule.urlRegexes) { - try { - new RegExp(regex); - } catch (error) { - return res.status(400).send({ error: `Invalid regex: ${regex}` }); - } - } - } - } - - await upsertSliseAdContainerRules(validatedRules); - - res.status(200).send({ message: 'Rules have been added successfully' }); - }) - ) - ) - .delete( - basicAuth(BasicAuthRights.ManageAds), - withExceptionHandler( - withBodyValidation(makeStringArraySchema('Domains should be an array of strings'), async (req, res) => { - const removedEntriesCount = await removeSliseAdContainerRules(req.body); - - res.status(200).send({ message: `${removedEntriesCount} entries have been removed` }); - }) - ) - ); +addObjectStorageMethodsToRouter( + sliseRulesRouter, + '/', + { + getByKey: getSliseAdContainerRulesByDomain, + getAllValues: getAllSliseAdContainerRules, + upsertValues: upsertSliseAdContainerRules, + removeValues: removeSliseAdContainerRules + }, + 'domain', + sliseAdContainerRulesDictionarySchema, + hostnamesListSchema, + removedEntriesCount => `${removedEntriesCount} domains have been removed`, + BasicAuthRights.ManageAds +); diff --git a/src/utils/express-helpers.ts b/src/utils/express-helpers.ts new file mode 100644 index 0000000..0a73d13 --- /dev/null +++ b/src/utils/express-helpers.ts @@ -0,0 +1,97 @@ +import { NextFunction, Request, RequestHandler, Response, Router } from 'express'; +import { ArraySchema as IArraySchema, ObjectSchema as IObjectSchema, Schema, ValidationError } from 'yup'; + +import { basicAuth, BasicAuthRights } from '../middlewares/basic-auth.middleware'; + +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) { + res.status(500).send({ error }); + } + }; + +export const addObjectStorageMethodsToRouter = ( + router: Router, + path: string, + methods: ObjectStorageMethods, + keyName: string, + objectValidationSchema: IObjectSchema>, + keysArrayValidationSchema: IArraySchema, + successfulRemovalMessage: (removedEntriesCount: number) => string, + modifyAuthRights: BasicAuthRights +) => { + 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(modifyAuthRights), + 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(modifyAuthRights), + 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..36b8c53 --- /dev/null +++ b/src/utils/schemas.ts @@ -0,0 +1,99 @@ +import { + array as arraySchema, + ArraySchema as IArraySchema, + boolean as booleanSchema, + object as objectSchema, + ObjectSchema as IObjectSchema, + string as stringSchema +} from 'yup'; + +import { SliseAdContainerRule } from '../advertising/slise'; +import { isValidSelectorsGroup } from '../utils/selectors.min.js'; +import { isDefined } from './helpers'; + +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()).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 sliseAdContainerRulesSchema = arraySchema().of( + objectSchema().shape({ + urlRegexes: arraySchema().of(regexStringSchema.clone().required()).required(), + selector: objectSchema().shape({ + isMultiple: booleanSchema().required(), + cssString: cssSelectorSchema.clone().required(), + shouldUseResultParent: booleanSchema().required(), + shouldUseDivWrapper: booleanSchema().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])$/ +); + +export const sliseAdContainerRulesDictionarySchema: IObjectSchema> = + objectSchema() + .test('keys-are-hostnames', async (value: object) => { + await Promise.all(Object.keys(value).map(hostname => hostnameSchema.validate(hostname))); + + return true; + }) + .test('values-are-valid', async (value: unknown) => { + if (typeof value !== 'object' || value === null) { + return true; + } + + await Promise.all(Object.values(value).map(rules => sliseAdContainerRulesSchema.validate(rules))); + + return true; + }) + .required(); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const hostnamesListSchema: IArraySchema = arraySchema() + .of(hostnameSchema.clone().required()) + .required(); + +const adTypeSchema = stringSchema().min(1).required(); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const adTypesListSchema: IArraySchema = arraySchema() + .of(adTypeSchema.clone()) + .min(1) + .required() + .typeError('Must be a non-empty string'); + +const selectorsListSchema = arraySchema().of(cssSelectorSchema.clone().required()).required(); + +export const sliseSelectorsDictionarySchema: IObjectSchema> = objectSchema() + .test('keys-are-valid', async (value: object) => { + await Promise.all(Object.keys(value).map(adType => adTypeSchema.clone().validate(adType))); + + return true; + }) + .test('values-are-valid', async (value: object) => { + await Promise.all(Object.values(value).map(selectors => selectorsListSchema.validate(selectors))); + + return true; + }); diff --git a/src/utils/selectors.min.js b/src/utils/selectors.min.js new file mode 100644 index 0000000..12470b9 --- /dev/null +++ b/src/utils/selectors.min.js @@ -0,0 +1,3 @@ +/*! 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 3474dfa..219e686 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3574,6 +3574,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" @@ -4233,6 +4238,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" @@ -4245,6 +4255,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" @@ -4339,6 +4354,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" @@ -4616,6 +4636,16 @@ yocto-queue@^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" From d4010ed8e37fc614c958134420aaab28a15b162f Mon Sep 17 00:00:00 2001 From: Inokentii Mazhara Date: Tue, 12 Dec 2023 16:31:52 +0200 Subject: [PATCH 03/14] TW-1192 Leave one login/password pair --- .env.dist | 6 ++-- package.json | 1 - src/config.ts | 12 +++---- src/index.ts | 4 +-- src/middlewares/basic-auth.middleware.ts | 19 +++------- src/routers/slise-heuristic-rules-router.ts | 9 +++-- src/routers/slise-rules-router.ts | 4 +-- src/utils/express-helpers.ts | 9 +++-- yarn.lock | 40 --------------------- 9 files changed, 22 insertions(+), 82 deletions(-) diff --git a/.env.dist b/.env.dist index 57b8115..5f4b337 100644 --- a/.env.dist +++ b/.env.dist @@ -8,7 +8,5 @@ ALICE_BOB_PRIVATE_KEY= THREE_ROUTE_API_URL= THREE_ROUTE_API_AUTH_TOKEN= REDIS_URL= -ADD_NOTIFICATION_USERNAME= -ADD_NOTIFICATION_PASSWORD= -MANAGE_ADS_USERNAME= -MANAGE_ADS_PASSWORD= +ADMIN_USERNAME= +ADMIN_PASSWORD= diff --git a/package.json b/package.json index 5f0cf0d..eb51313 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "express": "^4.18.2", "firebase-admin": "^10.0.2", "ioredis": "^5.3.2", - "joi": "^17.11.0", "lodash": "^4.17.21", "memoizee": "^0.4.15", "pino": "^6.11.2", diff --git a/src/config.ts b/src/config.ts index a0f7f21..7ed45ca 100644 --- a/src/config.ts +++ b/src/config.ts @@ -5,16 +5,14 @@ export const MIN_IOS_APP_VERSION = '1.10.445'; export const MIN_ANDROID_APP_VERSION = '1.10.445'; export const EnvVars = { - MOONPAY_SECRET_KEY: '', // getEnv('MOONPAY_SECRET_KEY'), - ALICE_BOB_PRIVATE_KEY: '', // getEnv('ALICE_BOB_PRIVATE_KEY'), - ALICE_BOB_PUBLIC_KEY: '', // getEnv('ALICE_BOB_PUBLIC_KEY'), + MOONPAY_SECRET_KEY: getEnv('MOONPAY_SECRET_KEY'), + ALICE_BOB_PRIVATE_KEY: getEnv('ALICE_BOB_PRIVATE_KEY'), + ALICE_BOB_PUBLIC_KEY: getEnv('ALICE_BOB_PUBLIC_KEY'), 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'), - MANAGE_ADS_USERNAME: getEnv('MANAGE_ADS_USERNAME'), - MANAGE_ADS_PASSWORD: getEnv('MANAGE_ADS_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 030074e..e232f90 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,7 +12,7 @@ import swaggerUi from 'swagger-ui-express'; import { getAdvertisingInfo } from './advertising/advertising'; import { MIN_ANDROID_APP_VERSION, MIN_IOS_APP_VERSION } from './config'; import getDAppsStats from './getDAppsStats'; -import { basicAuth, BasicAuthRights } from './middlewares/basic-auth.middleware'; +import { basicAuth } from './middlewares/basic-auth.middleware'; import { Notification, PlatformType } from './notifications/notification.interface'; import { getImageFallback } from './notifications/utils/get-image-fallback.util'; import { getNotifications } from './notifications/utils/get-notifications.util'; @@ -118,7 +118,7 @@ app.get('/api/notifications', async (_req, res) => { } }); -app.post('/api/notifications', basicAuth(BasicAuthRights.AddNotification), async (req, res) => { +app.post('/api/notifications', basicAuth, async (req, res) => { try { const { mobile, diff --git a/src/middlewares/basic-auth.middleware.ts b/src/middlewares/basic-auth.middleware.ts index 28eb93a..deaca01 100644 --- a/src/middlewares/basic-auth.middleware.ts +++ b/src/middlewares/basic-auth.middleware.ts @@ -3,28 +3,17 @@ import { Request, Response, NextFunction } from 'express'; import { EnvVars } from '../config'; import { isDefined } from '../utils/helpers'; -export enum BasicAuthRights { - AddNotification = 'add-notification', - ManageAds = 'manage-ads' -} - const credentials = { - [BasicAuthRights.AddNotification]: { - username: EnvVars.ADD_NOTIFICATION_USERNAME, - password: EnvVars.ADD_NOTIFICATION_PASSWORD - }, - [BasicAuthRights.ManageAds]: { - username: EnvVars.MANAGE_ADS_USERNAME, - password: EnvVars.MANAGE_ADS_PASSWORD - } + username: EnvVars.ADMIN_USERNAME, + password: EnvVars.ADMIN_PASSWORD }; -export const basicAuth = (rights: BasicAuthRights) => (req: Request, res: Response, next: NextFunction) => { +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[rights]; + const { username: correctUsername, password: correctPassword } = credentials; if (!(username === correctUsername && password === correctPassword)) { handleNotAuthenticated(res, next); diff --git a/src/routers/slise-heuristic-rules-router.ts b/src/routers/slise-heuristic-rules-router.ts index 43a350f..497f223 100644 --- a/src/routers/slise-heuristic-rules-router.ts +++ b/src/routers/slise-heuristic-rules-router.ts @@ -9,7 +9,7 @@ import { removeSliseHeuristicUrlRegexes, upsertSliseHeuristicSelectors } from '../advertising/slise'; -import { basicAuth, BasicAuthRights } from '../middlewares/basic-auth.middleware'; +import { basicAuth } from '../middlewares/basic-auth.middleware'; import { addObjectStorageMethodsToRouter, withBodyValidation, withExceptionHandler } from '../utils/express-helpers'; import { adTypesListSchema, regexStringListSchema, sliseSelectorsDictionarySchema } from '../utils/schemas'; @@ -93,7 +93,7 @@ sliseHeuristicRulesRouter }) ) .post( - basicAuth(BasicAuthRights.ManageAds), + basicAuth, withExceptionHandler( withBodyValidation(regexStringListSchema, async (req, res) => { const regexesAddedCount = await addSliseHeuristicUrlRegexes(req.body); @@ -103,7 +103,7 @@ sliseHeuristicRulesRouter ) ) .delete( - basicAuth(BasicAuthRights.ManageAds), + basicAuth, withExceptionHandler( withBodyValidation(regexStringListSchema, async (req, res) => { const regexesRemovedCount = await removeSliseHeuristicUrlRegexes(req.body); @@ -206,6 +206,5 @@ addObjectStorageMethodsToRouter( 'adType', sliseSelectorsDictionarySchema, adTypesListSchema, - removedEntriesCount => `${removedEntriesCount} ad types have been removed`, - BasicAuthRights.ManageAds + removedEntriesCount => `${removedEntriesCount} ad types have been removed` ); diff --git a/src/routers/slise-rules-router.ts b/src/routers/slise-rules-router.ts index 31d3b27..5b4a400 100644 --- a/src/routers/slise-rules-router.ts +++ b/src/routers/slise-rules-router.ts @@ -7,7 +7,6 @@ import { SliseAdContainerRule, upsertSliseAdContainerRules } from '../advertising/slise'; -import { BasicAuthRights } from '../middlewares/basic-auth.middleware'; import { addObjectStorageMethodsToRouter } from '../utils/express-helpers'; import { hostnamesListSchema, sliseAdContainerRulesDictionarySchema } from '../utils/schemas'; import { sliseHeuristicRulesRouter } from './slise-heuristic-rules-router'; @@ -216,6 +215,5 @@ addObjectStorageMethodsToRouter( 'domain', sliseAdContainerRulesDictionarySchema, hostnamesListSchema, - removedEntriesCount => `${removedEntriesCount} domains have been removed`, - BasicAuthRights.ManageAds + removedEntriesCount => `${removedEntriesCount} domains have been removed` ); diff --git a/src/utils/express-helpers.ts b/src/utils/express-helpers.ts index 0a73d13..3499acd 100644 --- a/src/utils/express-helpers.ts +++ b/src/utils/express-helpers.ts @@ -1,7 +1,7 @@ import { NextFunction, Request, RequestHandler, Response, Router } from 'express'; import { ArraySchema as IArraySchema, ObjectSchema as IObjectSchema, Schema, ValidationError } from 'yup'; -import { basicAuth, BasicAuthRights } from '../middlewares/basic-auth.middleware'; +import { basicAuth } from '../middlewares/basic-auth.middleware'; interface ObjectStorageMethods { getByKey: (key: string) => Promise; @@ -49,8 +49,7 @@ export const addObjectStorageMethodsToRouter = ( keyName: string, objectValidationSchema: IObjectSchema>, keysArrayValidationSchema: IArraySchema, - successfulRemovalMessage: (removedEntriesCount: number) => string, - modifyAuthRights: BasicAuthRights + successfulRemovalMessage: (removedEntriesCount: number) => string ) => { router.get( path === '/' ? `/:${keyName}` : `${path}/:${keyName}`, @@ -73,7 +72,7 @@ export const addObjectStorageMethodsToRouter = ( }) ) .post( - basicAuth(modifyAuthRights), + basicAuth, withExceptionHandler( withBodyValidation(objectValidationSchema, async (req, res) => { const validatedValues = req.body; @@ -85,7 +84,7 @@ export const addObjectStorageMethodsToRouter = ( ) ) .delete( - basicAuth(modifyAuthRights), + basicAuth, withExceptionHandler( withBodyValidation(keysArrayValidationSchema, async (req, res) => { const removedEntriesCount = await methods.removeValues(req.body); diff --git a/yarn.lock b/yarn.lock index 219e686..12090f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -236,18 +236,6 @@ resolved "https://registry.yarnpkg.com/@hapi/bourne/-/bourne-2.0.0.tgz#5bb2193eb685c0007540ca61d166d4e1edaf918d" integrity sha512-WEezM1FWztfbzqIUbsDzFRVMxSoLy3HugVcux6KDDtTqzPsLE8NDRHfXvev66aH1i2oOKKar3/XDjbvh/OUBdg== -"@hapi/hoek@^9.0.0": - version "9.3.0" - resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb" - integrity sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ== - -"@hapi/topo@^5.0.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.1.0.tgz#dc448e332c6c6e37a4dc02fd84ba8d44b9afb012" - integrity sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg== - dependencies: - "@hapi/hoek" "^9.0.0" - "@humanwhocodes/config-array@^0.5.0": version "0.5.0" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" @@ -369,23 +357,6 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= -"@sideway/address@^4.1.3": - version "4.1.4" - resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0" - integrity sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw== - dependencies: - "@hapi/hoek" "^9.0.0" - -"@sideway/formula@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" - integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== - -"@sideway/pinpoint@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@sideway/pinpoint/-/pinpoint-2.0.0.tgz#cff8ffadc372ad29fd3f78277aeb29e632cc70df" - integrity sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ== - "@stablelib/binary@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@stablelib/binary/-/binary-1.0.1.tgz#c5900b94368baf00f811da5bdb1610963dfddf7f" @@ -2822,17 +2793,6 @@ jmespath@^0.15.0: resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc= -joi@^17.11.0: - version "17.11.0" - resolved "https://registry.yarnpkg.com/joi/-/joi-17.11.0.tgz#aa9da753578ec7720e6f0ca2c7046996ed04fc1a" - integrity sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ== - dependencies: - "@hapi/hoek" "^9.0.0" - "@hapi/topo" "^5.0.0" - "@sideway/address" "^4.1.3" - "@sideway/formula" "^3.0.1" - "@sideway/pinpoint" "^2.0.0" - jose@^2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/jose/-/jose-2.0.6.tgz#894ba19169af339d3911be933f913dd02fc57c7c" From e97aa06a24e287eebfff16ebe7912a75be7b616e Mon Sep 17 00:00:00 2001 From: Inokentii Mazhara Date: Tue, 12 Dec 2023 16:46:09 +0200 Subject: [PATCH 04/14] TW-1192 Disable eslint for selectors.min.js --- src/utils/selectors.min.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/selectors.min.js b/src/utils/selectors.min.js index 12470b9..12f6ca4 100644 --- a/src/utils/selectors.min.js +++ b/src/utils/selectors.min.js @@ -1,3 +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; From 1a9be167390b5b6748cadb8d32c7002046a8b4de Mon Sep 17 00:00:00 2001 From: Inokentii Mazhara Date: Wed, 13 Dec 2023 17:39:04 +0200 Subject: [PATCH 05/14] TW-1192 Remake API to match requirements of TW-1222 --- src/advertising/slise.ts | 52 ++- src/index.ts | 6 +- .../ad-places.ts} | 125 +++--- src/routers/slise-ad-rules/index.ts | 43 ++ src/routers/slise-ad-rules/providers.ts | 370 ++++++++++++++++++ src/routers/slise-heuristic-rules-router.ts | 210 ---------- src/utils/schemas.ts | 101 ++--- 7 files changed, 551 insertions(+), 356 deletions(-) rename src/routers/{slise-rules-router.ts => slise-ad-rules/ad-places.ts} (55%) create mode 100644 src/routers/slise-ad-rules/index.ts create mode 100644 src/routers/slise-ad-rules/providers.ts delete mode 100644 src/routers/slise-heuristic-rules-router.ts diff --git a/src/advertising/slise.ts b/src/advertising/slise.ts index 037a721..2851051 100644 --- a/src/advertising/slise.ts +++ b/src/advertising/slise.ts @@ -1,18 +1,25 @@ import { redisClient } from '../redis'; import { isDefined } from '../utils/helpers'; -export interface SliseAdContainerRule { +export interface SliseAdPlacesRule { urlRegexes: string[]; selector: { isMultiple: boolean; cssString: string; shouldUseResultParent: boolean; + shouldUseDivWrapper: boolean; }; } -const SLISE_AD_CONTAINERS_RULES_KEY = 'slise_ad_containers_rules'; -const SLISE_HEURISTIC_URL_REGEXES_KEY = 'slise_heuristic_url_regexes_key'; -const SLISE_HEURISTIC_SELECTORS_KEY = 'slise_heuristic_selectors_key'; +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 => { @@ -39,23 +46,30 @@ const objectStorageMethodsFactory = (storageKey: string, fallbackValue: V) => }); export const { - getByKey: getSliseAdContainerRulesByDomain, - getAllValues: getAllSliseAdContainerRules, - upsertValues: upsertSliseAdContainerRules, - removeValues: removeSliseAdContainerRules -} = objectStorageMethodsFactory(SLISE_AD_CONTAINERS_RULES_KEY, []); + 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: getSliseHeuristicSelectorsByAdType, - getAllValues: getAllSliseHeuristicSelectors, - upsertValues: upsertSliseHeuristicSelectors, - removeValues: removeSliseHeuristicSelectors -} = objectStorageMethodsFactory(SLISE_HEURISTIC_SELECTORS_KEY, []); + getByKey: getSelectorsByProviderId, + getAllValues: getAllProviders, + upsertValues: upsertProviders, + removeValues: removeProviders +} = objectStorageMethodsFactory(SLISE_AD_PROVIDERS_LIST_KEY, []); -export const getSliseHeuristicUrlRegexes = async () => redisClient.smembers(SLISE_HEURISTIC_URL_REGEXES_KEY); +export const getSliseAdProvidersForAllSites = async () => redisClient.smembers(SLISE_AD_PROVIDERS_ALL_SITES_KEY); -export const addSliseHeuristicUrlRegexes = async (regexes: string[]) => - redisClient.sadd(SLISE_HEURISTIC_URL_REGEXES_KEY, ...regexes); +export const addSliseAdProvidersForAllSites = async (providers: string[]) => + redisClient.sadd(SLISE_AD_PROVIDERS_ALL_SITES_KEY, ...providers); -export const removeSliseHeuristicUrlRegexes = async (regexes: string[]) => - redisClient.srem(SLISE_HEURISTIC_URL_REGEXES_KEY, ...regexes); +export const removeSliseAdProvidersForAllSites = async (providers: string[]) => + redisClient.srem(SLISE_AD_PROVIDERS_ALL_SITES_KEY, ...providers); diff --git a/src/index.ts b/src/index.ts index e232f90..6046687 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,7 +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-rules-router'; +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'; @@ -325,7 +325,7 @@ app.get('/api/advertising-info', (_req, res) => { } }); -app.use('/api/slise-ad-container-rules', sliseRulesRouter); +app.use('/api/slise-ad-rules', sliseRulesRouter); const swaggerOptions = { swaggerDefinition: { @@ -335,7 +335,7 @@ const swaggerOptions = { version: '1.0.0' } }, - apis: ['./src/index.ts', './src/routers/*.ts'] + apis: ['./src/index.ts', './src/routers/**/*.ts'] }; const swaggerSpec = swaggerJSDoc(swaggerOptions); app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); diff --git a/src/routers/slise-rules-router.ts b/src/routers/slise-ad-rules/ad-places.ts similarity index 55% rename from src/routers/slise-rules-router.ts rename to src/routers/slise-ad-rules/ad-places.ts index 5b4a400..c3c48b2 100644 --- a/src/routers/slise-rules-router.ts +++ b/src/routers/slise-ad-rules/ad-places.ts @@ -1,50 +1,19 @@ import { Router } from 'express'; import { - getAllSliseAdContainerRules, - getSliseAdContainerRulesByDomain, - removeSliseAdContainerRules, - SliseAdContainerRule, - upsertSliseAdContainerRules -} from '../advertising/slise'; -import { addObjectStorageMethodsToRouter } from '../utils/express-helpers'; -import { hostnamesListSchema, sliseAdContainerRulesDictionarySchema } from '../utils/schemas'; -import { sliseHeuristicRulesRouter } from './slise-heuristic-rules-router'; + getAllSliseAdPlacesRules, + getSliseAdPlacesRulesByDomain, + removeSliseAdPlacesRules, + upsertSliseAdPlacesRules +} from '../../advertising/slise'; +import { addObjectStorageMethodsToRouter } from '../../utils/express-helpers'; +import { hostnamesListSchema, sliseAdPlacesRulesDictionarySchema } from '../../utils/schemas'; /** * @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 * schemas: - * SliseAdContainerSelector: + * SliseAdPlacesRuleSelector: * type: object * required: * - isMultiple @@ -64,7 +33,7 @@ import { sliseHeuristicRulesRouter } from './slise-heuristic-rules-router'; * shouldUseDivWrapper: * type: boolean * description: Whether the ads banner should be wrapped in a div - * SliseAdContainerRule: + * SliseAdPlacesRule: * type: object * required: * - urlRegexes @@ -75,16 +44,22 @@ import { sliseHeuristicRulesRouter } from './slise-heuristic-rules-router'; * items: * type: string * format: regex - * description: List of regexes to match the site URL against * selector: - * $ref: '#/components/schemas/SliseAdContainerSelector' - * SliseAdContainerRulesDictionary: + * $ref: '#/components/schemas/SliseAdPlacesRuleSelector' + * example: + * urlRegexes: + * - '^https://goerli\.etherscan\.io/?$' + * selector: + * isMultiple: false + * cssString: 'main > section div.row > div:nth-child(2) > div' + * shouldUseResultParent: false + * shouldUseDivWrapper: false + * SliseAdPlacesRulesDictionary: * type: object * additionalProperties: * type: array * items: - * $ref: '#/components/schemas/SliseAdContainerRule' - * description: Dictionary of rules for domains + * $ref: '#/components/schemas/SliseAdPlacesRule' * example: * goerli.etherscan.io: * - urlRegexes: @@ -109,66 +84,56 @@ import { sliseHeuristicRulesRouter } from './slise-heuristic-rules-router'; * cssString: 'div.left-container > app-pe-banner:nth-child(2)' * shouldUseResultParent: false * shouldUseDivWrapper: true - * SliseAdTypesSelectorsDictionary: - * type: object - * additionalProperties: - * type: array - * items: - * type: string - * example: - * coinzilla: - * - 'iframe[src*="coinzilla.io"]' - * - 'iframe[src*="czilladx.com"]' */ -export const sliseRulesRouter = Router(); - -sliseRulesRouter.use('/heuristic', sliseHeuristicRulesRouter); +export const sliseAdPlacesRulesRouter = Router(); /** * @swagger - * /api/slise-ad-container-rules/{domain}: + * /api/slise-ad-rules/ad-places/{domain}: * get: - * summary: Get Slise ad container rule for specified domain + * summary: Get rules for ads places for the specified domain * parameters: * - in: path * name: domain * required: true - * format: hostname * schema: * type: string + * format: hostname * example: 'goerli.etherscan.io' * responses: * '200': - * description: Slise ad container rules for the specified domain + * description: Rules list * content: * application/json: * schema: - * $ref: '#/components/schemas/SliseAdContainerRule' + * type: array + * items: + * $ref: '#/components/schemas/SliseAdPlacesRule' * '500': * $ref: '#/components/responses/ErrorResponse' - * /api/slise-ad-container-rules: + * /api/slise-ad-rules/ad-places: * get: - * summary: Get all Slise ad container rules + * summary: Get all rules for ads places * responses: * '200': - * description: List of Slise ad container rules + * description: Domain - rules list dictionary * content: * application/json: * schema: - * $ref: '#/components/schemas/SliseAdContainerRulesDictionary' + * $ref: '#/components/schemas/SliseAdPlacesRulesDictionary' * '500': * $ref: '#/components/responses/ErrorResponse' * post: - * summary: Upserts Slise ad container rules. Rules for domains that have existed before will be overwritten + * summary: Add rules for ads places * security: * - basicAuth: [] * requestBody: - * description: Domain - rules list dictionary of rules + * description: Domain - rules list dictionary * content: * application/json: * schema: - * $ref: '#/components/schemas/SliseAdContainerRulesDictionary' + * $ref: '#/components/schemas/SliseAdPlacesRulesDictionary' * responses: * '200': * $ref: '#/components/responses/SuccessResponse' @@ -179,11 +144,11 @@ sliseRulesRouter.use('/heuristic', sliseHeuristicRulesRouter); * '500': * $ref: '#/components/responses/ErrorResponse' * delete: - * summary: Delete specified Slise ad container rules + * summary: Remove rules for ads places * security: * - basicAuth: [] * requestBody: - * description: List of rule IDs to delete + * description: List of domain names to remove rules for * content: * application/json: * schema: @@ -203,17 +168,17 @@ sliseRulesRouter.use('/heuristic', sliseHeuristicRulesRouter); * '500': * $ref: '#/components/responses/ErrorResponse' */ -addObjectStorageMethodsToRouter( - sliseRulesRouter, +addObjectStorageMethodsToRouter( + sliseAdPlacesRulesRouter, '/', { - getByKey: getSliseAdContainerRulesByDomain, - getAllValues: getAllSliseAdContainerRules, - upsertValues: upsertSliseAdContainerRules, - removeValues: removeSliseAdContainerRules + getByKey: getSliseAdPlacesRulesByDomain, + getAllValues: getAllSliseAdPlacesRules, + upsertValues: upsertSliseAdPlacesRules, + removeValues: removeSliseAdPlacesRules }, 'domain', - sliseAdContainerRulesDictionarySchema, + sliseAdPlacesRulesDictionarySchema, hostnamesListSchema, - removedEntriesCount => `${removedEntriesCount} domains have been removed` + 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..db265ff --- /dev/null +++ b/src/routers/slise-ad-rules/providers.ts @@ -0,0 +1,370 @@ +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. + * 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 + * 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/routers/slise-heuristic-rules-router.ts b/src/routers/slise-heuristic-rules-router.ts deleted file mode 100644 index 497f223..0000000 --- a/src/routers/slise-heuristic-rules-router.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { Router } from 'express'; - -import { - addSliseHeuristicUrlRegexes, - getAllSliseHeuristicSelectors, - getSliseHeuristicSelectorsByAdType, - getSliseHeuristicUrlRegexes, - removeSliseHeuristicSelectors, - removeSliseHeuristicUrlRegexes, - upsertSliseHeuristicSelectors -} from '../advertising/slise'; -import { basicAuth } from '../middlewares/basic-auth.middleware'; -import { addObjectStorageMethodsToRouter, withBodyValidation, withExceptionHandler } from '../utils/express-helpers'; -import { adTypesListSchema, regexStringListSchema, sliseSelectorsDictionarySchema } from '../utils/schemas'; - -export const sliseHeuristicRulesRouter = Router(); - -/** - * @swagger - * /api/slise-ad-container-rules/heuristic/url-regexes: - * get: - * summary: Get regexes for pages URLs where heuristic search, i.e. by provider, should be used - * responses: - * '200': - * description: List of regexes - * content: - * application/json: - * schema: - * type: array - * items: - * type: string - * format: regex - * example: - * - '^https://www\.dextools\.io/app/[A-z]{2}/[0-9A-z-]+/pair-explorer' - * '500': - * $ref: '#/components/responses/ErrorResponse' - * post: - * summary: Add regexes for pages URLs where heuristic search, i.e. by provider, should be used - * security: - * - basicAuth: [] - * requestBody: - * description: List of regexes - * content: - * application/json: - * schema: - * type: array - * items: - * type: string - * format: regex - * example: - * - '^https://www\.dextools\.io/app/[A-z]{2}/[0-9A-z-]+/pair-explorer' - * responses: - * '200': - * $ref: '#/components/responses/SuccessResponse' - * '400': - * $ref: '#/components/responses/ErrorResponse' - * '401': - * $ref: '#/components/responses/UnauthorizedError' - * '500': - * $ref: '#/components/responses/ErrorResponse' - * delete: - * summary: Remove regexes for pages URLs where heuristic search, i.e. by provider, should be used - * security: - * - basicAuth: [] - * requestBody: - * description: List of regexes - * content: - * application/json: - * schema: - * type: array - * items: - * type: string - * format: regex - * example: - * - '^https://www\.dextools\.io/app/[A-z]{2}/[0-9A-z-]+/pair-explorer' - * responses: - * '200': - * $ref: '#/components/responses/SuccessResponse' - * '400': - * $ref: '#/components/responses/ErrorResponse' - * '401': - * $ref: '#/components/responses/UnauthorizedError' - * '500': - * $ref: '#/components/responses/ErrorResponse' - */ -sliseHeuristicRulesRouter - .route('/url-regexes') - .get( - withExceptionHandler(async (_req, res) => { - const regexes = await getSliseHeuristicUrlRegexes(); - - res.status(200).send(regexes); - }) - ) - .post( - basicAuth, - withExceptionHandler( - withBodyValidation(regexStringListSchema, async (req, res) => { - const regexesAddedCount = await addSliseHeuristicUrlRegexes(req.body); - - res.status(200).send({ message: `${regexesAddedCount} regexes have been added` }); - }) - ) - ) - .delete( - basicAuth, - withExceptionHandler( - withBodyValidation(regexStringListSchema, async (req, res) => { - const regexesRemovedCount = await removeSliseHeuristicUrlRegexes(req.body); - - res.status(200).send({ message: `${regexesRemovedCount} regexes have been removed` }); - }) - ) - ); - -/** - * @swagger - * /api/slise-ad-container-rules/heuristic/selectors/{adType}: - * get: - * summary: Get CSS selectors for heuristic search for specified ad type - * parameters: - * - in: path - * name: adType - * required: true - * type: string - * example: 'coinzilla' - * responses: - * '200': - * description: List of CSS selectors - * content: - * application/json: - * schema: - * type: array - * items: - * type: string - * example: - * - 'iframe[src*="coinzilla.io"]' - * - 'iframe[src*="czilladx.com"]' - * '500': - * $ref: '#/components/responses/ErrorResponse' - * /api/slise-ad-container-rules/heuristic/selectors: - * get: - * summary: Get CSS selectors for heuristic search for all ads types - * responses: - * '200': - * description: Ad type - selectors list dictionary - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/SliseAdTypesSelectorsDictionary' - * '500': - * $ref: '#/components/responses/ErrorResponse' - * post: - * summary: Upserts CSS selectors for heuristic search. Selectors for ad types that have existed before will be overwritten - * security: - * - basicAuth: [] - * requestBody: - * description: Ad type - selectors list dictionary - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/SliseAdTypesSelectorsDictionary' - * responses: - * '200': - * $ref: '#/components/responses/SuccessResponse' - * '400': - * $ref: '#/components/responses/ErrorResponse' - * '401': - * $ref: '#/components/responses/UnauthorizedError' - * '500': - * $ref: '#/components/responses/ErrorResponse' - * delete: - * summary: Remove CSS selectors for heuristic search - * security: - * - basicAuth: [] - * requestBody: - * description: List of ads types - * 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( - sliseHeuristicRulesRouter, - '/selectors', - { - getByKey: getSliseHeuristicSelectorsByAdType, - getAllValues: getAllSliseHeuristicSelectors, - upsertValues: upsertSliseHeuristicSelectors, - removeValues: removeSliseHeuristicSelectors - }, - 'adType', - sliseSelectorsDictionarySchema, - adTypesListSchema, - removedEntriesCount => `${removedEntriesCount} ad types have been removed` -); diff --git a/src/utils/schemas.ts b/src/utils/schemas.ts index 36b8c53..4568605 100644 --- a/src/utils/schemas.ts +++ b/src/utils/schemas.ts @@ -4,10 +4,12 @@ import { boolean as booleanSchema, object as objectSchema, ObjectSchema as IObjectSchema, - string as stringSchema + Schema, + string as stringSchema, + StringSchema as IStringSchema } from 'yup'; -import { SliseAdContainerRule } from '../advertising/slise'; +import { SliseAdPlacesRule, SliseAdProvidersByDomainRule } from '../advertising/slise'; import { isValidSelectorsGroup } from '../utils/selectors.min.js'; import { isDefined } from './helpers'; @@ -25,7 +27,7 @@ const regexStringSchema = stringSchema().test('is-regex', function (value: strin } }); -export const regexStringListSchema = arraySchema().of(regexStringSchema.clone().required()).required(); +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 @@ -36,64 +38,75 @@ const cssSelectorSchema = stringSchema().test('is-css-selector', function (value throw this.createError({ path: this.path, message: `${value} must be a valid CSS selector` }); }); -const sliseAdContainerRulesSchema = arraySchema().of( - objectSchema().shape({ - urlRegexes: arraySchema().of(regexStringSchema.clone().required()).required(), - selector: objectSchema().shape({ - isMultiple: booleanSchema().required(), - cssString: cssSelectorSchema.clone().required(), - shouldUseResultParent: booleanSchema().required(), - shouldUseDivWrapper: booleanSchema().required() - }) - }) -); +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])$/ ); -export const sliseAdContainerRulesDictionarySchema: IObjectSchema> = - objectSchema() - .test('keys-are-hostnames', async (value: object) => { - await Promise.all(Object.keys(value).map(hostname => hostnameSchema.validate(hostname))); - - return true; - }) - .test('values-are-valid', async (value: unknown) => { - if (typeof value !== 'object' || value === null) { - return true; - } - - await Promise.all(Object.values(value).map(rules => sliseAdContainerRulesSchema.validate(rules))); - - return true; - }) - .required(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any export const hostnamesListSchema: IArraySchema = arraySchema() .of(hostnameSchema.clone().required()) .required(); -const adTypeSchema = stringSchema().min(1).required(); +const adTypeSchema = stringSchema().min(1); // eslint-disable-next-line @typescript-eslint/no-explicit-any export const adTypesListSchema: IArraySchema = arraySchema() - .of(adTypeSchema.clone()) + .of(adTypeSchema.clone().required()) .min(1) .required() .typeError('Must be a non-empty string'); -const selectorsListSchema = arraySchema().of(cssSelectorSchema.clone().required()).required(); +const sliseAdPlacesRulesSchema = arraySchema() + .of( + objectSchema() + .shape({ + urlRegexes: arraySchema().of(regexStringSchema.clone().required()).required(), + selector: objectSchema().shape({ + isMultiple: booleanSchema().required(), + cssString: cssSelectorSchema.clone().required(), + shouldUseResultParent: booleanSchema().required(), + shouldUseDivWrapper: booleanSchema().required() + }) + }) + .required() + ) + .required(); -export const sliseSelectorsDictionarySchema: IObjectSchema> = objectSchema() - .test('keys-are-valid', async (value: object) => { - await Promise.all(Object.keys(value).map(adType => adTypeSchema.clone().validate(adType))); +const makeDictionarySchema = (keySchema: IStringSchema, valueSchema: Schema) => + objectSchema() + .test('keys-are-valid', async (value: object) => { + await Promise.all(Object.keys(value).map(key => keySchema.validate(key))); - return true; - }) - .test('values-are-valid', async (value: object) => { - await Promise.all(Object.values(value).map(selectors => selectorsListSchema.validate(selectors))); + return true; + }) + .test('values-are-valid', async (value: object) => { + await Promise.all(Object.values(value).map(value => valueSchema.validate(value))); - return true; - }); + return true; + }) + .required() as IObjectSchema>; + +export const sliseAdPlacesRulesDictionarySchema: IObjectSchema> = + makeDictionarySchema(hostnameSchema, sliseAdPlacesRulesSchema); + +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); + +export const sliseAdProvidersDictionarySchema: IObjectSchema> = makeDictionarySchema( + adTypeSchema.clone().required(), + cssSelectorsListSchema.clone().required() +); From 3db84a4481b316109597c9a72b43467a67b9fe7c Mon Sep 17 00:00:00 2001 From: Inokentii Mazhara Date: Wed, 13 Dec 2023 17:44:51 +0200 Subject: [PATCH 06/14] TW-1192 Update docs --- src/routers/slise-ad-rules/providers.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/routers/slise-ad-rules/providers.ts b/src/routers/slise-ad-rules/providers.ts index db265ff..c7056bd 100644 --- a/src/routers/slise-ad-rules/providers.ts +++ b/src/routers/slise-ad-rules/providers.ts @@ -97,7 +97,7 @@ export const sliseAdProvidersRouter = Router(); * 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. + * from lists of providers from specific sites. Checks for providers existence are not performed * security: * - basicAuth: [] * requestBody: @@ -212,7 +212,9 @@ sliseAdProvidersRouter * '500': * $ref: '#/components/responses/ErrorResponse' * post: - * summary: Add rules for providers of ads for the specified sites + * 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: From b1fa2973e08627c4d26a4c6c7e8427967c37272c Mon Sep 17 00:00:00 2001 From: Inokentii Mazhara Date: Fri, 15 Dec 2023 11:45:58 +0200 Subject: [PATCH 07/14] TW-1192 Replace 'shouldUseParent' property with 'parentDepth' --- src/advertising/slise.ts | 2 +- src/routers/slise-ad-rules/ad-places.ts | 19 +++++++++++-------- src/utils/schemas.ts | 3 ++- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/advertising/slise.ts b/src/advertising/slise.ts index 2851051..7bb4591 100644 --- a/src/advertising/slise.ts +++ b/src/advertising/slise.ts @@ -6,7 +6,7 @@ export interface SliseAdPlacesRule { selector: { isMultiple: boolean; cssString: string; - shouldUseResultParent: boolean; + parentDepth: number; shouldUseDivWrapper: boolean; }; } diff --git a/src/routers/slise-ad-rules/ad-places.ts b/src/routers/slise-ad-rules/ad-places.ts index c3c48b2..2ab3404 100644 --- a/src/routers/slise-ad-rules/ad-places.ts +++ b/src/routers/slise-ad-rules/ad-places.ts @@ -18,7 +18,7 @@ import { hostnamesListSchema, sliseAdPlacesRulesDictionarySchema } from '../../u * required: * - isMultiple * - cssString - * - shouldUseResultParent + * - parentDepth * - shouldUseDivWrapper * properties: * isMultiple: @@ -27,9 +27,12 @@ import { hostnamesListSchema, sliseAdPlacesRulesDictionarySchema } from '../../u * cssString: * type: string * description: CSS selector - * shouldUseResultParent: - * type: boolean - * description: Whether the results parents should be used as ads containers + * parentDepth: + * type: number + * description: > + * Indicates the depth of the parent element of the selected element, i. e. 0 means that the selected + * elements are ads containers themselves, 1 means that the selected elements are ads containers' direct + * children and so on. * shouldUseDivWrapper: * type: boolean * description: Whether the ads banner should be wrapped in a div @@ -52,7 +55,7 @@ import { hostnamesListSchema, sliseAdPlacesRulesDictionarySchema } from '../../u * selector: * isMultiple: false * cssString: 'main > section div.row > div:nth-child(2) > div' - * shouldUseResultParent: false + * parentDepth: 0 * shouldUseDivWrapper: false * SliseAdPlacesRulesDictionary: * type: object @@ -67,7 +70,7 @@ import { hostnamesListSchema, sliseAdPlacesRulesDictionarySchema } from '../../u * selector: * isMultiple: false * cssString: 'main > section div.row > div:nth-child(2) > div' - * shouldUseResultParent: false + * parentDepth: 0 * shouldUseDivWrapper: false * www.dextools.io: * - urlRegexes: @@ -75,14 +78,14 @@ import { hostnamesListSchema, sliseAdPlacesRulesDictionarySchema } from '../../u * selector: * isMultiple: true * cssString: 'app-header-banner' - * shouldUseResultParent: true + * 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)' - * shouldUseResultParent: false + * parentDepth: 0 * shouldUseDivWrapper: true */ diff --git a/src/utils/schemas.ts b/src/utils/schemas.ts index 4568605..aafe02c 100644 --- a/src/utils/schemas.ts +++ b/src/utils/schemas.ts @@ -2,6 +2,7 @@ import { array as arraySchema, ArraySchema as IArraySchema, boolean as booleanSchema, + number as numberSchema, object as objectSchema, ObjectSchema as IObjectSchema, Schema, @@ -66,7 +67,7 @@ const sliseAdPlacesRulesSchema = arraySchema() selector: objectSchema().shape({ isMultiple: booleanSchema().required(), cssString: cssSelectorSchema.clone().required(), - shouldUseResultParent: booleanSchema().required(), + parentDepth: numberSchema().integer().min(0).required(), shouldUseDivWrapper: booleanSchema().required() }) }) From 8f45259587dab09ed01ac7a8d43e8fe62ff9796c Mon Sep 17 00:00:00 2001 From: Inokentii Mazhara Date: Tue, 19 Dec 2023 12:04:01 +0200 Subject: [PATCH 08/14] TW-1192 Add some properties for SliseAdPlacesRule and nested objects --- src/advertising/slise.ts | 78 +++++++++++++++++++++++++ src/routers/slise-ad-rules/ad-places.ts | 30 +++++++++- src/utils/express-helpers.ts | 2 + src/utils/schemas.ts | 76 ++++++++++++++++-------- 4 files changed, 160 insertions(+), 26 deletions(-) diff --git a/src/advertising/slise.ts b/src/advertising/slise.ts index 7bb4591..36638b9 100644 --- a/src/advertising/slise.ts +++ b/src/advertising/slise.ts @@ -1,6 +1,82 @@ 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: { @@ -8,7 +84,9 @@ export interface SliseAdPlacesRule { cssString: string; parentDepth: number; shouldUseDivWrapper: boolean; + divWrapperStyle?: Record; }; + stylesOverrides?: SliseAdStylesOverrides[]; } export interface SliseAdProvidersByDomainRule { diff --git a/src/routers/slise-ad-rules/ad-places.ts b/src/routers/slise-ad-rules/ad-places.ts index 2ab3404..45a3324 100644 --- a/src/routers/slise-ad-rules/ad-places.ts +++ b/src/routers/slise-ad-rules/ad-places.ts @@ -29,6 +29,8 @@ import { hostnamesListSchema, sliseAdPlacesRulesDictionarySchema } from '../../u * 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 containers themselves, 1 means that the selected elements are ads containers' direct @@ -36,6 +38,28 @@ import { hostnamesListSchema, sliseAdPlacesRulesDictionarySchema } from '../../u * shouldUseDivWrapper: * type: boolean * description: Whether the 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 the selected element that should change its style. + * style: + * type: object + * description: New style of the parent element + * additionalProperties: + * type: string * SliseAdPlacesRule: * type: object * required: @@ -49,6 +73,10 @@ import { hostnamesListSchema, sliseAdPlacesRulesDictionarySchema } from '../../u * format: regex * selector: * $ref: '#/components/schemas/SliseAdPlacesRuleSelector' + * stylesOverrides: + * type: array + * items: + * $ref: '#/components/schemas/SliseAdStylesOverrides' * example: * urlRegexes: * - '^https://goerli\.etherscan\.io/?$' @@ -128,7 +156,7 @@ export const sliseAdPlacesRulesRouter = Router(); * '500': * $ref: '#/components/responses/ErrorResponse' * post: - * summary: Add rules for ads places + * summary: Add rules for ads places. If rules for a domain already exist, they will be overwritten * security: * - basicAuth: [] * requestBody: diff --git a/src/utils/express-helpers.ts b/src/utils/express-helpers.ts index 3499acd..8a6e05b 100644 --- a/src/utils/express-helpers.ts +++ b/src/utils/express-helpers.ts @@ -2,6 +2,7 @@ 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; @@ -38,6 +39,7 @@ export const withExceptionHandler = try { await handler(req, res, next); } catch (error) { + logger.error(error as object); res.status(500).send({ error }); } }; diff --git a/src/utils/schemas.ts b/src/utils/schemas.ts index aafe02c..5f887b4 100644 --- a/src/utils/schemas.ts +++ b/src/utils/schemas.ts @@ -10,10 +10,33 @@ import { StringSchema as IStringSchema } from 'yup'; -import { SliseAdPlacesRule, SliseAdProvidersByDomainRule } from '../advertising/slise'; +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)) { @@ -42,7 +65,8 @@ const cssSelectorSchema = stringSchema().test('is-css-selector', function (value 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])$/ + /^(([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 @@ -59,38 +83,40 @@ export const adTypesListSchema: IArraySchema = arraySchem .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() - }) + 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(); -const makeDictionarySchema = (keySchema: IStringSchema, valueSchema: Schema) => - objectSchema() - .test('keys-are-valid', async (value: object) => { - await Promise.all(Object.keys(value).map(key => keySchema.validate(key))); - - return true; - }) - .test('values-are-valid', async (value: object) => { - await Promise.all(Object.values(value).map(value => valueSchema.validate(value))); - - return true; - }) - .required() as IObjectSchema>; - export const sliseAdPlacesRulesDictionarySchema: IObjectSchema> = - makeDictionarySchema(hostnameSchema, sliseAdPlacesRulesSchema); + makeDictionarySchema(hostnameSchema, sliseAdPlacesRulesSchema).required(); const sliseAdProvidersByDomainRulesSchema = arraySchema() .of( @@ -105,9 +131,9 @@ const sliseAdProvidersByDomainRulesSchema = arraySchema() export const sliseAdProvidersByDomainsRulesDictionarySchema: IObjectSchema< Record -> = makeDictionarySchema(hostnameSchema, sliseAdProvidersByDomainRulesSchema); +> = makeDictionarySchema(hostnameSchema, sliseAdProvidersByDomainRulesSchema).required(); export const sliseAdProvidersDictionarySchema: IObjectSchema> = makeDictionarySchema( adTypeSchema.clone().required(), cssSelectorsListSchema.clone().required() -); +).required(); From 40176f509ae68dda8607eec7ddc30342f7d666b7 Mon Sep 17 00:00:00 2001 From: Inokentii Mazhara Date: Tue, 19 Dec 2023 12:41:39 +0200 Subject: [PATCH 09/14] TW-1192 Update docs --- src/routers/slise-ad-rules/ad-places.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/routers/slise-ad-rules/ad-places.ts b/src/routers/slise-ad-rules/ad-places.ts index 45a3324..d8bc841 100644 --- a/src/routers/slise-ad-rules/ad-places.ts +++ b/src/routers/slise-ad-rules/ad-places.ts @@ -33,11 +33,11 @@ import { hostnamesListSchema, sliseAdPlacesRulesDictionarySchema } from '../../u * integer: true * description: > * Indicates the depth of the parent element of the selected element, i. e. 0 means that the selected - * elements are ads containers themselves, 1 means that the selected elements are ads containers' direct + * 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 ads banner should be wrapped in a div + * description: Whether the Slise ads banner should be wrapped in a div * divWrapperStyle: * type: object * description: Style of the div wrapper @@ -54,7 +54,7 @@ import { hostnamesListSchema, sliseAdPlacesRulesDictionarySchema } from '../../u * min: 0 * integer: true * description: > - * Indicates the depth of the parent element for the selected element that should change its style. + * 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 From cd42663509cab3927b6c61208dce8c93406d7e23 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 21 Dec 2023 13:53:48 +0200 Subject: [PATCH 10/14] Revert "Update minimal app version (#132)" (#133) This reverts commit b2201fd04a3c0bba6aedf3b50d526f7d3bbe20c1. --- src/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config.ts b/src/config.ts index b920dce..4cfd913 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,8 +1,8 @@ import { getEnv } from './utils/env'; import { isDefined } from './utils/helpers'; -export const MIN_IOS_APP_VERSION = '1.19.1026'; -export const MIN_ANDROID_APP_VERSION = '1.19.1026'; +export const MIN_IOS_APP_VERSION = '1.10.445'; +export const MIN_ANDROID_APP_VERSION = '1.10.445'; export const EnvVars = { MOONPAY_SECRET_KEY: getEnv('MOONPAY_SECRET_KEY'), From 6f6521ed7be63be97c3eb731e30a7833b6ea6732 Mon Sep 17 00:00:00 2001 From: Inokentii Mazhara Date: Thu, 21 Dec 2023 16:27:53 +0200 Subject: [PATCH 11/14] TW-1235 Add 'opacity' style property --- src/advertising/slise.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/advertising/slise.ts b/src/advertising/slise.ts index 36638b9..ec30234 100644 --- a/src/advertising/slise.ts +++ b/src/advertising/slise.ts @@ -46,6 +46,7 @@ export const stylePropsNames = [ 'min-height', 'min-inline-size', 'min-width', + 'opacity', 'overflow', 'overflow-anchor', 'overflow-wrap', From b5169b56d289c03876d6aa0cd428d7f59ce4c153 Mon Sep 17 00:00:00 2001 From: Inokentii Mazhara Date: Mon, 25 Dec 2023 12:43:54 +0200 Subject: [PATCH 12/14] TW-1192: Implement a backend for ads replacement management (#130) * TW-1192 Implement a backend for ads replacement management * TW-1192 Input validation improvements * TW-1192 Leave one login/password pair * TW-1192 Disable eslint for selectors.min.js * TW-1192 Remake API to match requirements of TW-1222 * TW-1192 Update docs * TW-1192 Replace 'shouldUseParent' property with 'parentDepth' * TW-1192 Add some properties for SliseAdPlacesRule and nested objects * TW-1192 Update docs --- .env.dist | 4 +- package.json | 5 +- src/advertising/slise.ts | 153 ++++++++++ src/config.ts | 4 +- src/index.ts | 18 ++ src/middlewares/basic-auth.middleware.ts | 8 +- src/routers/slise-ad-rules/ad-places.ts | 215 +++++++++++++ src/routers/slise-ad-rules/index.ts | 43 +++ src/routers/slise-ad-rules/providers.ts | 372 +++++++++++++++++++++++ src/utils/express-helpers.ts | 98 ++++++ src/utils/schemas.ts | 139 +++++++++ src/utils/selectors.min.js | 4 + yarn.lock | 192 +++++++++++- 13 files changed, 1242 insertions(+), 13 deletions(-) create mode 100644 src/advertising/slise.ts create mode 100644 src/routers/slise-ad-rules/ad-places.ts create mode 100644 src/routers/slise-ad-rules/index.ts create mode 100644 src/routers/slise-ad-rules/providers.ts create mode 100644 src/utils/express-helpers.ts create mode 100644 src/utils/schemas.ts create mode 100644 src/utils/selectors.min.js 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" From 8eec0da2faab6d9ef2b5e36b647d1bf3cf16839f Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 5 Jan 2024 15:45:42 +0200 Subject: [PATCH 13/14] TW-1248: Force update users app version (#135) --- src/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config.ts b/src/config.ts index 7ed45ca..dbce743 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,8 +1,8 @@ import { getEnv } from './utils/env'; import { isDefined } from './utils/helpers'; -export const MIN_IOS_APP_VERSION = '1.10.445'; -export const MIN_ANDROID_APP_VERSION = '1.10.445'; +export const MIN_IOS_APP_VERSION = '1.19.1026'; +export const MIN_ANDROID_APP_VERSION = '1.19.1026'; export const EnvVars = { MOONPAY_SECRET_KEY: getEnv('MOONPAY_SECRET_KEY'), From 04dc8fe0c05e9cff4664a3894fd2044beb0014a8 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 8 Jan 2024 11:45:55 +0200 Subject: [PATCH 14/14] TW-1248: Force update users app version 1.20 (#136) --- src/config.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config.ts b/src/config.ts index dbce743..1088834 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,8 +1,8 @@ import { getEnv } from './utils/env'; import { isDefined } from './utils/helpers'; -export const MIN_IOS_APP_VERSION = '1.19.1026'; -export const MIN_ANDROID_APP_VERSION = '1.19.1026'; +export const MIN_IOS_APP_VERSION = '1.20.1027'; +export const MIN_ANDROID_APP_VERSION = '1.20.1027'; export const EnvVars = { MOONPAY_SECRET_KEY: getEnv('MOONPAY_SECRET_KEY'),