diff --git a/src/app/middleware/feature-limits.middleware.ts b/src/app/middleware/feature-limits.middleware.ts new file mode 100644 index 00000000..84acf05b --- /dev/null +++ b/src/app/middleware/feature-limits.middleware.ts @@ -0,0 +1,64 @@ +import { Request, Response, NextFunction } from 'express'; +import { AuthorizedUser } from '../routes/types'; + +type User = AuthorizedUser['user']; +type Middleware = (req: Request & { behalfUser?: User }, res: Response, next: NextFunction) => Promise; +type DataSource = { fieldName: string; sourceKey: string }; +type BuilderArgs = { + limitLabel: string; + dataSources: DataSource[]; +}; + +const LimitLabels = { + MaxFileUploadSize: 'max-file-upload-size', +}; + +const build = (Service: { + FeatureLimits: { + shouldLimitBeEnforced: (user: User, limitLabel: string, data: any) => Promise; + }; +}) => { + const mdBuilder = ({ limitLabel, dataSources }: BuilderArgs) => + (async (req, res, next) => { + try { + const user = (req as any).behalfUser || (req as AuthorizedUser).user; + + if (!user) { + return next(); + } + + const extractedData = extractDataFromRequest(req, dataSources); + const shouldLimitBeEnforced = await Service.FeatureLimits.shouldLimitBeEnforced( + user, + limitLabel, + extractedData, + ); + + if (shouldLimitBeEnforced) { + return res.status(402).send('You reached the limit for your tier!'); + } + + next(); + } catch (err) { + next(err); + } + }) as Middleware; + + return { + UploadFile: mdBuilder({ + limitLabel: LimitLabels.MaxFileUploadSize, + dataSources: [{ sourceKey: 'body', fieldName: 'file' }], + }), + }; +}; + +const extractDataFromRequest = (request: any, dataSources: DataSource[]) => { + const extractedData = {} as any; + for (const { sourceKey, fieldName } of dataSources) { + const value = request[sourceKey][fieldName]; + extractedData[fieldName] = value; + } + return extractedData; +}; + +export { build, LimitLabels }; diff --git a/src/app/models/index.ts b/src/app/models/index.ts index c95806d3..fa449b46 100644 --- a/src/app/models/index.ts +++ b/src/app/models/index.ts @@ -176,7 +176,6 @@ export default (database: Sequelize) => { Limit.belongsToMany(Tier, { through: TierLimit, - as: 'tiers', }); return { diff --git a/src/app/routes/storage.ts b/src/app/routes/storage.ts index 9cc29b7b..d5cc9dcb 100644 --- a/src/app/routes/storage.ts +++ b/src/app/routes/storage.ts @@ -21,6 +21,7 @@ import { FolderWithNameAlreadyExistsError } from '../services/errors/FolderWithNameAlreadyExistsError'; import * as resourceSharingMiddlewareBuilder from '../middleware/resource-sharing.middleware'; +import * as featureLimitsMiddlewareBuilder from '../middleware/feature-limits.middleware'; import {validate } from 'uuid'; type AuthorizedRequest = Request & { user: UserAttributes }; @@ -811,12 +812,14 @@ export default (router: Router, service: any) => { const sharedAdapter = sharedMiddlewareBuilder.build(service); const teamsAdapter = teamsMiddlewareBuilder.build(service); const resourceSharingAdapter = resourceSharingMiddlewareBuilder.build(service); + const featureLimitsAdapter = featureLimitsMiddlewareBuilder.build(service); const controller = new StorageController(service, Logger); router.post('/storage/file', passportAuth, sharedAdapter, resourceSharingAdapter.UploadFile, + featureLimitsAdapter.UploadFile, controller.createFile.bind(controller) ); router.post('/storage/file/exists', diff --git a/src/app/services/errors/FeatureLimitsErrors.ts b/src/app/services/errors/FeatureLimitsErrors.ts new file mode 100644 index 00000000..ce1d3b8f --- /dev/null +++ b/src/app/services/errors/FeatureLimitsErrors.ts @@ -0,0 +1,7 @@ +export class NoLimitFoundForUserTierAndLabel extends Error { + constructor(message: string) { + super(message); + + Object.setPrototypeOf(this, NoLimitFoundForUserTierAndLabel.prototype); + } +} diff --git a/src/app/services/featureLimit.js b/src/app/services/featureLimit.js index ba18fa55..b60b47aa 100644 --- a/src/app/services/featureLimit.js +++ b/src/app/services/featureLimit.js @@ -1,3 +1,10 @@ +const { LimitLabels } = require('../middleware/feature-limits.middleware'); +const { NoLimitFoundForUserTierAndLabel } = require('./errors/FeatureLimitsErrors'); + +const LimitTypes = { + Counter: 'counter', + Boolean: 'boolean', +}; module.exports = (Model, App) => { const getTierByPlanId = async (planId) => { @@ -7,8 +14,60 @@ module.exports = (Model, App) => { }); }; + const findLimitByLabelAndTier = async (tierId, label) => { + return Model.limits.findOne({ + where: { + label, + }, + include: [ + { + model: Model.tiers, + where: { + id: tierId, + }, + }, + ], + }); + }; + + const isBooleanLimitNotAvailable = (limit) => { + return limit.type === LimitTypes.Boolean && limit.value !== 'true'; + }; + + const shouldLimitBeEnforced = async (user, limitLabel, data) => { + const limit = await findLimitByLabelAndTier(user.tierId, limitLabel); + + if (!limit) { + throw new NoLimitFoundForUserTierAndLabel('No limit found for this user tier and label!'); + } + if (limit.type === LimitTypes.Boolean) { + return isBooleanLimitNotAvailable(limit); + } + + const isLimitSuprassed = await checkCounterLimit(user, limit, data); + + return isLimitSuprassed; + }; + + const checkCounterLimit = (user, limit, data) => { + switch (limit.label) { + case LimitLabels.MaxFileUploadSize: + return isMaxFileSizeLimitSurprassed({ limit, data }); + default: + return null; + } + }; + + const isMaxFileSizeLimitSurprassed = async ({ limit, data }) => { + const { + file: { size }, + } = data; + return Number(limit.value) < size / 1024 / 1024 / 1024; + }; + return { Name: 'FeatureLimits', getTierByPlanId, + shouldLimitBeEnforced, }; };