From 728e2e562a7c6e90e82f2b0c5078c2c6fff4876c Mon Sep 17 00:00:00 2001 From: "BENJAMIN\\bensh" Date: Thu, 3 Oct 2024 22:59:55 +0100 Subject: [PATCH 1/9] feat(express): Added pageSizeAllowOverride option --- src/core/domains/express/actions/resourceIndex.ts | 2 +- .../domains/express/interfaces/IRouteResourceOptions.ts | 1 + src/core/domains/express/services/Paginate.ts | 8 ++++++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/core/domains/express/actions/resourceIndex.ts b/src/core/domains/express/actions/resourceIndex.ts index 4a8223dd0..811dc0246 100644 --- a/src/core/domains/express/actions/resourceIndex.ts +++ b/src/core/domains/express/actions/resourceIndex.ts @@ -31,7 +31,7 @@ const formatResults = (results: IModel[]) => results.map(result => r */ export default async (req: BaseRequest, res: Response, options: IRouteResourceOptions): Promise => { try { - const paginate = new Paginate().parseRequest(req); + const paginate = new Paginate().parseRequest(req, options.paginate); const page = paginate.getPage(1); const pageSize = paginate.getPageSize() ?? options?.paginate?.pageSize; const skip = pageSize ? (page - 1) * pageSize : undefined; diff --git a/src/core/domains/express/interfaces/IRouteResourceOptions.ts b/src/core/domains/express/interfaces/IRouteResourceOptions.ts index 5ec225486..5c6166273 100644 --- a/src/core/domains/express/interfaces/IRouteResourceOptions.ts +++ b/src/core/domains/express/interfaces/IRouteResourceOptions.ts @@ -20,5 +20,6 @@ export interface IRouteResourceOptions extends Pick { allFilters?: object; paginate?: { pageSize: number; + allowPageSizeOverride?: boolean; } } \ No newline at end of file diff --git a/src/core/domains/express/services/Paginate.ts b/src/core/domains/express/services/Paginate.ts index 474183bc3..8bf3ae7d4 100644 --- a/src/core/domains/express/services/Paginate.ts +++ b/src/core/domains/express/services/Paginate.ts @@ -1,6 +1,10 @@ import Singleton from "@src/core/base/Singleton"; import { Request } from "express"; +export type ParseRequestOptions = { + allowPageSizeOverride?: boolean +} + class Paginate extends Singleton { protected page: number | undefined = undefined @@ -13,12 +17,12 @@ class Paginate extends Singleton { * @param {Request} req - The Express Request object * @returns {this} - The Paginate class itself to enable chaining */ - parseRequest(req: Request): this { + parseRequest(req: Request, options: ParseRequestOptions = { allowPageSizeOverride: true }): this { if(req.query?.page) { this.page = parseInt(req.query?.page as string); } - if(req.query?.pageSize) { + if(options.allowPageSizeOverride && req.query?.pageSize) { this.pageSize = parseInt(req.query?.pageSize as string); } From f5e5df9cc05be5ed86490bd854734906ed10ec3c Mon Sep 17 00:00:00 2001 From: "BENJAMIN\\bensh" Date: Sun, 6 Oct 2024 00:53:38 +0100 Subject: [PATCH 2/9] refactor(kernel): set app environment before loading providers --- src/core/Kernel.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/Kernel.ts b/src/core/Kernel.ts index 1b17df422..f44bbf24c 100644 --- a/src/core/Kernel.ts +++ b/src/core/Kernel.ts @@ -43,6 +43,8 @@ export default class Kernel extends Singleton const { appConfig } = kernel; + App.getInstance().env = appConfig.environment; + for (const provider of appConfig.providers) { if(withoutProviders.includes(provider.constructor.name)) { continue; @@ -60,7 +62,7 @@ export default class Kernel extends Singleton kernel.preparedProviders.push(provider.constructor.name); } - App.getInstance().env = appConfig.environment; + Kernel.getInstance().readyProviders = [...kernel.preparedProviders]; } From 5cd080239d44e53e65aa884c1ffdf5d5eddc648f Mon Sep 17 00:00:00 2001 From: "BENJAMIN\\bensh" Date: Sun, 6 Oct 2024 12:27:44 +0100 Subject: [PATCH 3/9] refactor(express): Refactored resource index into service --- .../domains/express/actions/baseAction.ts | 8 +- .../domains/express/actions/resourceAll.ts | 30 ++++ .../domains/express/actions/resourceIndex.ts | 106 ------------- .../express/interfaces/IResourceService.ts | 15 ++ .../domains/express/routing/RouteResource.ts | 6 +- .../services/Resources/ResourceAllService.ts | 144 ++++++++++++++++++ .../utils/stripGuardedResourceProperties.ts | 6 + 7 files changed, 200 insertions(+), 115 deletions(-) create mode 100644 src/core/domains/express/actions/resourceAll.ts delete mode 100644 src/core/domains/express/actions/resourceIndex.ts create mode 100644 src/core/domains/express/interfaces/IResourceService.ts create mode 100644 src/core/domains/express/services/Resources/ResourceAllService.ts create mode 100644 src/core/domains/express/utils/stripGuardedResourceProperties.ts diff --git a/src/core/domains/express/actions/baseAction.ts b/src/core/domains/express/actions/baseAction.ts index c6e18f9dc..c63f463f2 100644 --- a/src/core/domains/express/actions/baseAction.ts +++ b/src/core/domains/express/actions/baseAction.ts @@ -11,10 +11,6 @@ import { Response } from 'express'; * @param {IAction} action The action function that will be called with the BaseRequest, Response, and options. * @return {(req: BaseRequest, res: Response) => Promise} A new action function that calls the given action with the given options. */ -const ResourceAction = (options: IRouteResourceOptions, action: IAction) => { - return (req: BaseRequest, res: Response) => { - return action(req, res, options) - } -} +const baseAction = (options: IRouteResourceOptions, action: IAction) => (req: BaseRequest, res: Response) => action(req, res, options) -export default ResourceAction \ No newline at end of file +export default baseAction \ No newline at end of file diff --git a/src/core/domains/express/actions/resourceAll.ts b/src/core/domains/express/actions/resourceAll.ts new file mode 100644 index 000000000..a0b8a93c7 --- /dev/null +++ b/src/core/domains/express/actions/resourceAll.ts @@ -0,0 +1,30 @@ +import { IRouteResourceOptions } from '@src/core/domains/express/interfaces/IRouteResourceOptions'; +import responseError from '@src/core/domains/express/requests/responseError'; +import { BaseRequest } from "@src/core/domains/express/types/BaseRequest.t"; +import { Response } from 'express'; + +import ResourceAllService from '../services/Resources/ResourceAllService'; + + +/** + * Finds all records in the resource's repository + * + * @param {BaseRequest} req - The request object + * @param {Response} res - The response object + * @param {IRouteResourceOptions} options - The options object + * @returns {Promise} + */ +export default async (req: BaseRequest, res: Response, options: IRouteResourceOptions): Promise => { + try { + const resourceAllService = new ResourceAllService(); + resourceAllService.handler(req, res, options); + } + catch (err) { + if (err instanceof Error) { + responseError(req, res, err) + return; + } + + res.status(500).send({ error: 'Something went wrong' }) + } +} \ No newline at end of file diff --git a/src/core/domains/express/actions/resourceIndex.ts b/src/core/domains/express/actions/resourceIndex.ts deleted file mode 100644 index 811dc0246..000000000 --- a/src/core/domains/express/actions/resourceIndex.ts +++ /dev/null @@ -1,106 +0,0 @@ -import UnauthorizedError from '@src/core/domains/auth/exceptions/UnauthorizedError'; -import { IDocumentManager } from '@src/core/domains/database/interfaces/IDocumentManager'; -import { IRouteResourceOptions } from '@src/core/domains/express/interfaces/IRouteResourceOptions'; -import responseError from '@src/core/domains/express/requests/responseError'; -import { RouteResourceTypes } from '@src/core/domains/express/routing/RouteResource'; -import Paginate from '@src/core/domains/express/services/Paginate'; -import { ALWAYS } from '@src/core/domains/express/services/Security'; -import SecurityReader from '@src/core/domains/express/services/SecurityReader'; -import { SecurityIdentifiers } from '@src/core/domains/express/services/SecurityRules'; -import { BaseRequest } from "@src/core/domains/express/types/BaseRequest.t"; -import { IModel } from '@src/core/interfaces/IModel'; -import IModelData from '@src/core/interfaces/IModelData'; -import { App } from '@src/core/services/App'; -import { Response } from 'express'; - -/** - * Formats the results by excluding guarded properties - * - * @param results - * @returns - */ -const formatResults = (results: IModel[]) => results.map(result => result.getData({ excludeGuarded: true }) as IModel); - -/** - * Finds all records in the resource's repository - * - * @param {BaseRequest} req - The request object - * @param {Response} res - The response object - * @param {IRouteResourceOptions} options - The options object - * @returns {Promise} - */ -export default async (req: BaseRequest, res: Response, options: IRouteResourceOptions): Promise => { - try { - const paginate = new Paginate().parseRequest(req, options.paginate); - const page = paginate.getPage(1); - const pageSize = paginate.getPageSize() ?? options?.paginate?.pageSize; - const skip = pageSize ? (page - 1) * pageSize : undefined; - - const resourceOwnerSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.RESOURCE_OWNER, [RouteResourceTypes.ALL]) - const authorizationSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.AUTHORIZED, [RouteResourceTypes.ALL, ALWAYS]); - - if(authorizationSecurity && !authorizationSecurity.callback(req)) { - responseError(req, res, new UnauthorizedError(), 401) - return; - } - - const tableName = (new options.resource(null)).table; - - const documentManager = App.container('db').documentManager().table(tableName) as IDocumentManager; - - let results: IModel[] = []; - - const filters = options.allFilters ?? {}; - - /** - * When a resourceOwnerSecurity is defined, we need to find all records that are owned by the user - */ - if (resourceOwnerSecurity && authorizationSecurity) { - - const propertyKey = resourceOwnerSecurity.arguements?.key; - const userId = App.container('requestContext').getByRequest(req, 'userId'); - - if (!userId) { - responseError(req, res, new UnauthorizedError(), 401); - return; - } - - if (typeof propertyKey !== 'string') { - throw new Error('Malformed resourceOwner security. Expected parameter \'key\' to be a string but received ' + typeof propertyKey); - } - - results = await documentManager.findMany({ - filter: { - ...filters, - [propertyKey]: userId - }, - limit: pageSize, - skip, - }) - - res.send(formatResults(results)) - return; - } - - /** - * Finds all results without any restrictions - */ - results = await documentManager.findMany({ - filter: filters, - limit: pageSize, - skip, - }) - - const resultsAsModels = results.map((result) => new options.resource(result)); - - res.send(formatResults(resultsAsModels)) - } - catch (err) { - if (err instanceof Error) { - responseError(req, res, err) - return; - } - - res.status(500).send({ error: 'Something went wrong' }) - } -} \ No newline at end of file diff --git a/src/core/domains/express/interfaces/IResourceService.ts b/src/core/domains/express/interfaces/IResourceService.ts new file mode 100644 index 000000000..5bd8398f7 --- /dev/null +++ b/src/core/domains/express/interfaces/IResourceService.ts @@ -0,0 +1,15 @@ +/* eslint-disable no-unused-vars */ +import { Response } from "express"; + +import { BaseRequest } from "../types/BaseRequest.t"; +import { IRouteResourceOptions } from "./IRouteResourceOptions"; + +export interface IResourceService { + handler(req: BaseRequest, res: Response, options: IRouteResourceOptions): Promise +} + +export interface IPageOptions { + page: number; + pageSize?: number; + skip?: number; +} \ No newline at end of file diff --git a/src/core/domains/express/routing/RouteResource.ts b/src/core/domains/express/routing/RouteResource.ts index 83fbb8088..f3edf1961 100644 --- a/src/core/domains/express/routing/RouteResource.ts +++ b/src/core/domains/express/routing/RouteResource.ts @@ -1,8 +1,8 @@ import ModelScopes from "@src/core/domains/auth/services/ModelScopes"; import baseAction from "@src/core/domains/express/actions/baseAction"; +import resourceAll from "@src/core/domains/express/actions/resourceAll"; import resourceCreate from "@src/core/domains/express/actions/resourceCreate"; import resourceDelete from "@src/core/domains/express/actions/resourceDelete"; -import resourceIndex from "@src/core/domains/express/actions/resourceIndex"; import resourceShow from "@src/core/domains/express/actions/resourceShow"; import resourceUpdate from "@src/core/domains/express/actions/resourceUpdate"; import { IRoute } from "@src/core/domains/express/interfaces/IRoute"; @@ -54,14 +54,14 @@ const RouteResource = (options: IRouteResourceOptions): IRoute[] => { const routes = RouteGroup([ // Get all resources Route({ - name: `${path}.index`, + name: `${path}.all`, resourceType: RouteResourceTypes.ALL, scopes, scopesPartial: ModelScopes.getScopes(resource, ['read', 'all']), enableScopes, method: 'get', path: `/${path}`, - action: baseAction(options, resourceIndex), + action: baseAction(options, resourceAll), middlewares: options.middlewares, security: options.security }), diff --git a/src/core/domains/express/services/Resources/ResourceAllService.ts b/src/core/domains/express/services/Resources/ResourceAllService.ts new file mode 100644 index 000000000..db4d1e28e --- /dev/null +++ b/src/core/domains/express/services/Resources/ResourceAllService.ts @@ -0,0 +1,144 @@ +import UnauthorizedError from "@src/core/domains/auth/exceptions/UnauthorizedError"; +import { IModel } from "@src/core/interfaces/IModel"; +import { App } from "@src/core/services/App"; +import { Response } from "express"; + +import { IPageOptions, IResourceService } from "../../interfaces/IResourceService"; +import { IRouteResourceOptions } from "../../interfaces/IRouteResourceOptions"; +import responseError from "../../requests/responseError"; +import { RouteResourceTypes } from "../../routing/RouteResource"; +import { BaseRequest } from "../../types/BaseRequest.t"; +import stripGuardedResourceProperties from "../../utils/stripGuardedResourceProperties"; +import Paginate from "../Paginate"; +import { ALWAYS } from "../Security"; +import SecurityReader from "../SecurityReader"; +import { SecurityIdentifiers } from "../SecurityRules"; + + +class ResourceAllService implements IResourceService { + + /** + * Handles the resource all action + * - Validates that the request is authorized + * - If the resource owner security is enabled, adds the owner's id to the filters + * - Fetches the results using the filters and page options + * - Maps the results to models + * - Strips the guarded properties from the results + * - Sends the results back to the client + * @param req The request object + * @param res The response object + * @param options The resource options + */ + async handler(req: BaseRequest, res: Response, options: IRouteResourceOptions): Promise { + + // Check if the authorization security applies to this route and it is valid + if(!this.validateAuthorization(req, options)) { + responseError(req, res, new UnauthorizedError(), 401) + return; + } + + // Build the page options, filters + const pageOptions = this.buildPageOptions(req, options); + let filters = this.buildFilters(options); + + // Check if the resource owner security applies to this route and it is valid + // If it is valid, we add the owner's id to the filters + if(this.validateResourceOwner(req, options)) { + const resourceOwnerSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.RESOURCE_OWNER, [RouteResourceTypes.ALL]) + const propertyKey = resourceOwnerSecurity?.arguements?.key as string; + + filters = { + ...filters, + [propertyKey]: App.container('requestContext').getByRequest(req, 'userId') + } + } + + // Fetch the results + const results = await this.fetchResults(options, filters, pageOptions) + const resultsAsModels = results.map((result) => new options.resource(result)); + + // Send the results + res.send(stripGuardedResourceProperties(resultsAsModels)) + } + + /** + * Fetches the results from the database + * + * @param {object} filters - The filters to use when fetching the results + * @param {IPageOptions} pageOptions - The page options to use when fetching the results + * @returns {Promise} - A promise that resolves to the fetched results as an array of models + */ + async fetchResults(options: IRouteResourceOptions, filters: object, pageOptions: IPageOptions): Promise { + const tableName = (new options.resource).table; + const documentManager = App.container('db').documentManager().table(tableName); + + return await documentManager.findMany({ + filter: filters, + limit: pageOptions.pageSize, + skip: pageOptions.skip, + }) + } + + /** + * Checks if the request is authorized to perform the action and if the resource owner security is set + * + * @param {BaseRequest} req - The request object + * @param {IRouteResourceOptions} options - The options object + * @returns {boolean} - Whether the request is authorized and resource owner security is set + */ + validateResourceOwner(req: BaseRequest, options: IRouteResourceOptions): boolean { + const resourceOwnerSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.RESOURCE_OWNER, [RouteResourceTypes.ALL]) + + if(this.validateAuthorization(req, options) && resourceOwnerSecurity ) { + return true; + } + + return false; + } + + /** + * Checks if the request is authorized to perform the action + * + * @param {BaseRequest} req - The request object + * @param {IRouteResourceOptions} options - The options object + * @returns {boolean} - Whether the request is authorized + */ + validateAuthorization(req: BaseRequest, options: IRouteResourceOptions): boolean { + const authorizationSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.AUTHORIZED, [RouteResourceTypes.ALL, ALWAYS]); + + if(authorizationSecurity && !authorizationSecurity.callback(req)) { + return false; + } + + return true; + } + + /** + * Builds the filters object + * + * @param {IRouteResourceOptions} options - The options object + * @returns {object} - The filters object + */ + buildFilters(options: IRouteResourceOptions): object { + return options.allFilters ?? {}; + } + + /** + * Builds the page options + * + * @param {BaseRequest} req - The request object + * @param {IRouteResourceOptions} options - The options object + * @returns {IPageOptions} - An object containing the page number, page size, and skip + */ + buildPageOptions(req: BaseRequest, options: IRouteResourceOptions): IPageOptions { + const paginate = new Paginate().parseRequest(req, options.paginate); + const page = paginate.getPage(1); + const pageSize = paginate.getPageSize() ?? options?.paginate?.pageSize; + const skip = pageSize ? (page - 1) * pageSize : undefined; + + return { skip, page, pageSize }; + } + +} + +export default ResourceAllService; \ No newline at end of file diff --git a/src/core/domains/express/utils/stripGuardedResourceProperties.ts b/src/core/domains/express/utils/stripGuardedResourceProperties.ts new file mode 100644 index 000000000..4616f507c --- /dev/null +++ b/src/core/domains/express/utils/stripGuardedResourceProperties.ts @@ -0,0 +1,6 @@ +import { IModel } from "@src/core/interfaces/IModel"; +import IModelData from "@src/core/interfaces/IModelData"; + +const stripGuardedResourceProperties = (results: IModel[]) => results.map(result => result.getData({ excludeGuarded: true }) as IModel); + +export default stripGuardedResourceProperties \ No newline at end of file From 0f51ba46373944632f17ecedbb12989c62231999 Mon Sep 17 00:00:00 2001 From: "BENJAMIN\\bensh" Date: Sun, 6 Oct 2024 13:19:10 +0100 Subject: [PATCH 4/9] feat(express): Added ResourceErrorService --- .../domains/express/actions/resourceAll.ts | 9 +-- .../Resources/ResourceErrorService.ts | 58 +++++++++++++++++++ 2 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 src/core/domains/express/services/Resources/ResourceErrorService.ts diff --git a/src/core/domains/express/actions/resourceAll.ts b/src/core/domains/express/actions/resourceAll.ts index a0b8a93c7..c1d872fcb 100644 --- a/src/core/domains/express/actions/resourceAll.ts +++ b/src/core/domains/express/actions/resourceAll.ts @@ -1,9 +1,9 @@ import { IRouteResourceOptions } from '@src/core/domains/express/interfaces/IRouteResourceOptions'; -import responseError from '@src/core/domains/express/requests/responseError'; import { BaseRequest } from "@src/core/domains/express/types/BaseRequest.t"; import { Response } from 'express'; import ResourceAllService from '../services/Resources/ResourceAllService'; +import ResourceErrorService from '../services/Resources/ResourceErrorService'; /** @@ -20,11 +20,6 @@ export default async (req: BaseRequest, res: Response, options: IRouteResourceOp resourceAllService.handler(req, res, options); } catch (err) { - if (err instanceof Error) { - responseError(req, res, err) - return; - } - - res.status(500).send({ error: 'Something went wrong' }) + ResourceErrorService.handleError(req, res, err) } } \ No newline at end of file diff --git a/src/core/domains/express/services/Resources/ResourceErrorService.ts b/src/core/domains/express/services/Resources/ResourceErrorService.ts new file mode 100644 index 000000000..7b2d103c8 --- /dev/null +++ b/src/core/domains/express/services/Resources/ResourceErrorService.ts @@ -0,0 +1,58 @@ +import { EnvironmentDevelopment } from "@src/core/consts/Environment"; +import ForbiddenResourceError from "@src/core/domains/auth/exceptions/ForbiddenResourceError"; +import UnauthorizedError from "@src/core/domains/auth/exceptions/UnauthorizedError"; +import ModelNotFound from "@src/core/exceptions/ModelNotFound"; +import { App } from "@src/core/services/App"; +import { Response } from "express"; + +import responseError from "../../requests/responseError"; +import { BaseRequest } from "../../types/BaseRequest.t"; + +class ResourceErrorService { + + /** + * Handles an error by sending an appropriate error response to the client. + * + * If the error is a ModelNotFound, it will be sent as a 404. + * If the error is a ForbiddenResourceError, it will be sent as a 403. + * If the error is an UnauthorizedError, it will be sent as a 401. + * If the error is an Error, it will be sent as a 500. + * If the error is anything else, it will be sent as a 500. + * + * @param req The Express Request object + * @param res The Express Response object + * @param err The error to handle + */ + public static handleError(req: BaseRequest, res: Response, err: unknown):void { + if(err instanceof ModelNotFound) { + responseError(req, res, err, 404) + return; + } + + if(err instanceof ForbiddenResourceError) { + responseError(req, res, err, 403) + return; + } + + if(err instanceof UnauthorizedError) { + responseError(req, res, err, 401) + return; + } + + if (err instanceof Error) { + responseError(req, res, err) + return; + } + + let error = 'Something went wrong.' + + if(App.env() === EnvironmentDevelopment) { + error = (err as Error)?.message ?? error + } + + res.status(500).send({ error }) + } + +} + +export default ResourceErrorService \ No newline at end of file From d907b954eaff185313b81f2e32cee952d9b697f9 Mon Sep 17 00:00:00 2001 From: "BENJAMIN\\bensh" Date: Sun, 6 Oct 2024 13:19:48 +0100 Subject: [PATCH 5/9] refactor(express): Refactored resource show into a service --- .../domains/express/actions/resourceShow.ts | 85 +---------- .../express/interfaces/IResourceService.ts | 4 +- .../services/Resources/BaseResourceService.ts | 52 +++++++ .../services/Resources/ResourceAllService.ts | 56 ++----- .../services/Resources/ResourceShowService.ts | 139 ++++++++++++++++++ .../utils/stripGuardedResourceProperties.ts | 8 +- 6 files changed, 222 insertions(+), 122 deletions(-) create mode 100644 src/core/domains/express/services/Resources/BaseResourceService.ts create mode 100644 src/core/domains/express/services/Resources/ResourceShowService.ts diff --git a/src/core/domains/express/actions/resourceShow.ts b/src/core/domains/express/actions/resourceShow.ts index 454e764ef..abe8d5927 100644 --- a/src/core/domains/express/actions/resourceShow.ts +++ b/src/core/domains/express/actions/resourceShow.ts @@ -1,18 +1,10 @@ -import Repository from '@src/core/base/Repository'; -import ForbiddenResourceError from '@src/core/domains/auth/exceptions/ForbiddenResourceError'; -import UnauthorizedError from '@src/core/domains/auth/exceptions/UnauthorizedError'; import { IRouteResourceOptions } from '@src/core/domains/express/interfaces/IRouteResourceOptions'; -import responseError from '@src/core/domains/express/requests/responseError'; -import { RouteResourceTypes } from '@src/core/domains/express/routing/RouteResource'; -import { ALWAYS } from '@src/core/domains/express/services/Security'; -import SecurityReader from '@src/core/domains/express/services/SecurityReader'; -import { SecurityIdentifiers } from '@src/core/domains/express/services/SecurityRules'; import { BaseRequest } from "@src/core/domains/express/types/BaseRequest.t"; -import ModelNotFound from '@src/core/exceptions/ModelNotFound'; -import { IModel } from '@src/core/interfaces/IModel'; -import { App } from '@src/core/services/App'; import { Response } from 'express'; +import ResourceErrorService from '../services/Resources/ResourceErrorService'; +import ResourceShowService from '../services/Resources/ResourceShowService'; + /** * Finds a resource by id * @@ -23,75 +15,10 @@ import { Response } from 'express'; */ export default async (req: BaseRequest, res: Response, options: IRouteResourceOptions): Promise => { try { - const resourceOwnerSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.RESOURCE_OWNER, [RouteResourceTypes.SHOW]); - const authorizationSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.AUTHORIZED, [RouteResourceTypes.SHOW, ALWAYS]); - - if(authorizationSecurity && !authorizationSecurity.callback(req)) { - responseError(req, res, new UnauthorizedError(), 401) - return; - } - const repository = new Repository(options.resource); - - let result: IModel | null = null; - - // Define our query filters - const filters: object = { - ...(options.showFilters ?? {}), - id: req.params?.id - }; - - /** - * When a resourceOwnerSecurity is defined, we need to find the record that is owned by the user - */ - if(resourceOwnerSecurity && authorizationSecurity) { - - const propertyKey = resourceOwnerSecurity.arguements?.key; - const userId = App.container('requestContext').getByRequest(req, 'userId'); - - if(!userId) { - responseError(req, res, new ForbiddenResourceError(), 403); - return; - } - - if(typeof propertyKey !== 'string') { - throw new Error('Malformed resourceOwner security. Expected parameter \'key\' to be a string but received ' + typeof propertyKey); - } - - result = await repository.findOne({ - ...filters, - [propertyKey]: userId - }) - - if (!result) { - throw new ModelNotFound('Resource not found'); - } - - res.send(result?.getData({ excludeGuarded: true }) as IModel); - - return; - } - - /** - * Find resource without restrictions - */ - result = await repository.findOne(filters); - - if (!result) { - throw new ModelNotFound('Resource not found'); - } - - res.send(result?.getData({ excludeGuarded: true }) as IModel); + const resourceShowService = new ResourceShowService() + resourceShowService.handler(req, res, options) } catch (err) { - if(err instanceof ModelNotFound) { - responseError(req, res, err, 404) - return; - } - if (err instanceof Error) { - responseError(req, res, err) - return; - } - - res.status(500).send({ error: 'Something went wrong' }) + ResourceErrorService.handleError(req, res, err) } } \ No newline at end of file diff --git a/src/core/domains/express/interfaces/IResourceService.ts b/src/core/domains/express/interfaces/IResourceService.ts index 5bd8398f7..89dc36918 100644 --- a/src/core/domains/express/interfaces/IResourceService.ts +++ b/src/core/domains/express/interfaces/IResourceService.ts @@ -4,8 +4,10 @@ import { Response } from "express"; import { BaseRequest } from "../types/BaseRequest.t"; import { IRouteResourceOptions } from "./IRouteResourceOptions"; +export type IPartialRouteResourceOptions = Omit + export interface IResourceService { - handler(req: BaseRequest, res: Response, options: IRouteResourceOptions): Promise + handler(req: BaseRequest, res: Response, options: IPartialRouteResourceOptions): Promise } export interface IPageOptions { diff --git a/src/core/domains/express/services/Resources/BaseResourceService.ts b/src/core/domains/express/services/Resources/BaseResourceService.ts new file mode 100644 index 000000000..83accd955 --- /dev/null +++ b/src/core/domains/express/services/Resources/BaseResourceService.ts @@ -0,0 +1,52 @@ +import { Response } from "express"; + +import { IResourceService } from "../../interfaces/IResourceService"; +import { IRouteResourceOptions } from "../../interfaces/IRouteResourceOptions"; +import { RouteResourceTypes } from "../../routing/RouteResource"; +import { BaseRequest } from "../../types/BaseRequest.t"; +import { ALWAYS } from "../Security"; +import SecurityReader from "../SecurityReader"; +import { SecurityIdentifiers } from "../SecurityRules"; + +abstract class BaseResourceService implements IResourceService { + + // eslint-disable-next-line no-unused-vars + abstract handler(req: BaseRequest, res: Response, options: IRouteResourceOptions): Promise; + + /** + * Checks if the request is authorized to perform the action and if the resource owner security is set + * + * @param {BaseRequest} req - The request object + * @param {IRouteResourceOptions} options - The options object + * @returns {boolean} - Whether the request is authorized and resource owner security is set + */ + validateResourceOwner(req: BaseRequest, options: IRouteResourceOptions): boolean { + const resourceOwnerSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.RESOURCE_OWNER, [RouteResourceTypes.ALL]) + + if(this.validateAuthorization(req, options) && resourceOwnerSecurity ) { + return true; + } + + return false; + } + + /** + * Checks if the request is authorized to perform the action + * + * @param {BaseRequest} req - The request object + * @param {IRouteResourceOptions} options - The options object + * @returns {boolean} - Whether the request is authorized + */ + validateAuthorization(req: BaseRequest, options: IRouteResourceOptions): boolean { + const authorizationSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.AUTHORIZED, [RouteResourceTypes.ALL, ALWAYS]); + + if(authorizationSecurity && !authorizationSecurity.callback(req)) { + return false; + } + + return true; + } + +} + +export default BaseResourceService \ No newline at end of file diff --git a/src/core/domains/express/services/Resources/ResourceAllService.ts b/src/core/domains/express/services/Resources/ResourceAllService.ts index db4d1e28e..d3e068232 100644 --- a/src/core/domains/express/services/Resources/ResourceAllService.ts +++ b/src/core/domains/express/services/Resources/ResourceAllService.ts @@ -1,21 +1,21 @@ +import ForbiddenResourceError from "@src/core/domains/auth/exceptions/ForbiddenResourceError"; import UnauthorizedError from "@src/core/domains/auth/exceptions/UnauthorizedError"; import { IModel } from "@src/core/interfaces/IModel"; import { App } from "@src/core/services/App"; import { Response } from "express"; -import { IPageOptions, IResourceService } from "../../interfaces/IResourceService"; +import { IPageOptions } from "../../interfaces/IResourceService"; import { IRouteResourceOptions } from "../../interfaces/IRouteResourceOptions"; -import responseError from "../../requests/responseError"; import { RouteResourceTypes } from "../../routing/RouteResource"; import { BaseRequest } from "../../types/BaseRequest.t"; import stripGuardedResourceProperties from "../../utils/stripGuardedResourceProperties"; import Paginate from "../Paginate"; -import { ALWAYS } from "../Security"; import SecurityReader from "../SecurityReader"; import { SecurityIdentifiers } from "../SecurityRules"; +import BaseResourceService from "./BaseResourceService"; -class ResourceAllService implements IResourceService { +class ResourceAllService extends BaseResourceService { /** * Handles the resource all action @@ -33,8 +33,7 @@ class ResourceAllService implements IResourceService { // Check if the authorization security applies to this route and it is valid if(!this.validateAuthorization(req, options)) { - responseError(req, res, new UnauthorizedError(), 401) - return; + throw new UnauthorizedError() } // Build the page options, filters @@ -46,10 +45,19 @@ class ResourceAllService implements IResourceService { if(this.validateResourceOwner(req, options)) { const resourceOwnerSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.RESOURCE_OWNER, [RouteResourceTypes.ALL]) const propertyKey = resourceOwnerSecurity?.arguements?.key as string; + const userId = App.container('requestContext').getByRequest(req, 'userId'); + + if(!userId) { + throw new ForbiddenResourceError() + } + + if(typeof propertyKey !== 'string') { + throw new Error('Malformed resourceOwner security. Expected parameter \'key\' to be a string but received ' + typeof propertyKey); + } filters = { ...filters, - [propertyKey]: App.container('requestContext').getByRequest(req, 'userId') + [propertyKey]: userId } } @@ -79,40 +87,6 @@ class ResourceAllService implements IResourceService { }) } - /** - * Checks if the request is authorized to perform the action and if the resource owner security is set - * - * @param {BaseRequest} req - The request object - * @param {IRouteResourceOptions} options - The options object - * @returns {boolean} - Whether the request is authorized and resource owner security is set - */ - validateResourceOwner(req: BaseRequest, options: IRouteResourceOptions): boolean { - const resourceOwnerSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.RESOURCE_OWNER, [RouteResourceTypes.ALL]) - - if(this.validateAuthorization(req, options) && resourceOwnerSecurity ) { - return true; - } - - return false; - } - - /** - * Checks if the request is authorized to perform the action - * - * @param {BaseRequest} req - The request object - * @param {IRouteResourceOptions} options - The options object - * @returns {boolean} - Whether the request is authorized - */ - validateAuthorization(req: BaseRequest, options: IRouteResourceOptions): boolean { - const authorizationSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.AUTHORIZED, [RouteResourceTypes.ALL, ALWAYS]); - - if(authorizationSecurity && !authorizationSecurity.callback(req)) { - return false; - } - - return true; - } - /** * Builds the filters object * diff --git a/src/core/domains/express/services/Resources/ResourceShowService.ts b/src/core/domains/express/services/Resources/ResourceShowService.ts new file mode 100644 index 000000000..3ef211681 --- /dev/null +++ b/src/core/domains/express/services/Resources/ResourceShowService.ts @@ -0,0 +1,139 @@ +import ForbiddenResourceError from "@src/core/domains/auth/exceptions/ForbiddenResourceError"; +import UnauthorizedError from "@src/core/domains/auth/exceptions/UnauthorizedError"; +import ModelNotFound from "@src/core/exceptions/ModelNotFound"; +import { IModel } from "@src/core/interfaces/IModel"; +import { App } from "@src/core/services/App"; +import { Response } from "express"; + +import { IPageOptions, IResourceService } from "../../interfaces/IResourceService"; +import { IRouteResourceOptions } from "../../interfaces/IRouteResourceOptions"; +import { RouteResourceTypes } from "../../routing/RouteResource"; +import { BaseRequest } from "../../types/BaseRequest.t"; +import stripGuardedResourceProperties from "../../utils/stripGuardedResourceProperties"; +import { ALWAYS } from "../Security"; +import SecurityReader from "../SecurityReader"; +import { SecurityIdentifiers } from "../SecurityRules"; + + +class ResourceShowService implements IResourceService { + + + /** + * Handles the resource show action + * - Validates that the request is authorized + * - If the resource owner security is enabled, adds the owner's id to the filters + * - Fetches the result using the filters + * - Maps the result to a model + * - Strips the guarded properties from the result + * - Sends the result back to the client + * @param req The request object + * @param res The response object + * @param options The resource options + */ + async handler(req: BaseRequest, res: Response, options: IRouteResourceOptions): Promise { + + // Check if the authorization security applies to this route and it is valid + if(!this.validateAuthorization(req, options)) { + throw new UnauthorizedError() + } + + // Build the page options, filters + let filters = this.buildFilters(options); + + // Check if the resource owner security applies to this route and it is valid + // If it is valid, we add the owner's id to the filters + if(this.validateResourceOwner(req, options)) { + const resourceOwnerSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.RESOURCE_OWNER, [RouteResourceTypes.ALL]) + const propertyKey = resourceOwnerSecurity?.arguements?.key as string; + const userId = App.container('requestContext').getByRequest(req, 'userId'); + + if(!userId) { + throw new ForbiddenResourceError() + } + + if(typeof propertyKey !== 'string') { + throw new Error('Malformed resourceOwner security. Expected parameter \'key\' to be a string but received ' + typeof propertyKey); + } + + filters = { + ...filters, + [propertyKey]: userId + } + } + + // Fetch the results + const result = await this.fetchRecord(options, filters) + + if (!result) { + throw new ModelNotFound(); + } + + const resultAsModel = new options.resource(result) + + // Send the results + res.send(stripGuardedResourceProperties(resultAsModel)) + } + + /** + * Fetches the results from the database + * + * @param {object} filters - The filters to use when fetching the results + * @param {IPageOptions} pageOptions - The page options to use when fetching the results + * @returns {Promise} - A promise that resolves to the fetched results as an array of models + */ + async fetchRecord(options: IRouteResourceOptions, filters: object): Promise { + const tableName = (new options.resource).table; + const documentManager = App.container('db').documentManager().table(tableName); + + return await documentManager.findOne({ + filter: filters, + }) + } + + /** + * Checks if the request is authorized to perform the action and if the resource owner security is set + * + * @param {BaseRequest} req - The request object + * @param {IRouteResourceOptions} options - The options object + * @returns {boolean} - Whether the request is authorized and resource owner security is set + */ + validateResourceOwner(req: BaseRequest, options: IRouteResourceOptions): boolean { + const resourceOwnerSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.RESOURCE_OWNER, [RouteResourceTypes.ALL]) + + if(this.validateAuthorization(req, options) && resourceOwnerSecurity ) { + return true; + } + + return false; + } + + /** + * Checks if the request is authorized to perform the action + * + * @param {BaseRequest} req - The request object + * @param {IRouteResourceOptions} options - The options object + * @returns {boolean} - Whether the request is authorized + */ + validateAuthorization(req: BaseRequest, options: IRouteResourceOptions): boolean { + const authorizationSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.AUTHORIZED, [RouteResourceTypes.ALL, ALWAYS]); + + if(authorizationSecurity && !authorizationSecurity.callback(req)) { + return false; + } + + return true; + } + + /** + * Builds the filters object + * + * @param {IRouteResourceOptions} options - The options object + * @returns {object} - The filters object + */ + buildFilters(options: IRouteResourceOptions): object { + return options.showFilters ?? {}; + } + +} + +export default ResourceShowService; \ No newline at end of file diff --git a/src/core/domains/express/utils/stripGuardedResourceProperties.ts b/src/core/domains/express/utils/stripGuardedResourceProperties.ts index 4616f507c..52bf82835 100644 --- a/src/core/domains/express/utils/stripGuardedResourceProperties.ts +++ b/src/core/domains/express/utils/stripGuardedResourceProperties.ts @@ -1,6 +1,12 @@ import { IModel } from "@src/core/interfaces/IModel"; import IModelData from "@src/core/interfaces/IModelData"; -const stripGuardedResourceProperties = (results: IModel[]) => results.map(result => result.getData({ excludeGuarded: true }) as IModel); +const stripGuardedResourceProperties = (results: IModel[] | IModel) => { + if(!Array.isArray(results)) { + return results.getData({ excludeGuarded: true }) as IModel + } + + return results.map(result => result.getData({ excludeGuarded: true }) as IModel); +} export default stripGuardedResourceProperties \ No newline at end of file From 7f78407835d1ca20a9430c9719893da2a4308b90 Mon Sep 17 00:00:00 2001 From: "BENJAMIN\\bensh" Date: Sun, 6 Oct 2024 13:38:37 +0100 Subject: [PATCH 6/9] refactor(express): Refactored resource create into a service --- .../domains/express/actions/resourceCreate.ts | 57 ++-------------- .../services/Resources/BaseResourceService.ts | 21 +++++- .../services/Resources/ResourceAllService.ts | 6 +- .../Resources/ResourceCreateService.ts | 65 +++++++++++++++++++ .../services/Resources/ResourceShowService.ts | 47 ++------------ 5 files changed, 98 insertions(+), 98 deletions(-) create mode 100644 src/core/domains/express/services/Resources/ResourceCreateService.ts diff --git a/src/core/domains/express/actions/resourceCreate.ts b/src/core/domains/express/actions/resourceCreate.ts index 1da654d30..bc8e38e99 100644 --- a/src/core/domains/express/actions/resourceCreate.ts +++ b/src/core/domains/express/actions/resourceCreate.ts @@ -1,15 +1,10 @@ -import UnauthorizedError from '@src/core/domains/auth/exceptions/UnauthorizedError'; -import MissingSecurityError from '@src/core/domains/express/exceptions/MissingSecurityError'; import { IRouteResourceOptions } from '@src/core/domains/express/interfaces/IRouteResourceOptions'; -import responseError from '@src/core/domains/express/requests/responseError'; -import { RouteResourceTypes } from '@src/core/domains/express/routing/RouteResource'; -import { ALWAYS } from '@src/core/domains/express/services/Security'; -import SecurityReader from '@src/core/domains/express/services/SecurityReader'; -import { SecurityIdentifiers } from '@src/core/domains/express/services/SecurityRules'; import { BaseRequest } from "@src/core/domains/express/types/BaseRequest.t"; -import { App } from '@src/core/services/App'; import { Response } from 'express'; +import ResourceCreateService from '../services/Resources/ResourceCreateService'; +import ResourceErrorService from '../services/Resources/ResourceErrorService'; + /** * Creates a new instance of the model @@ -21,50 +16,10 @@ import { Response } from 'express'; */ export default async (req: BaseRequest, res: Response, options: IRouteResourceOptions): Promise => { try { - const resourceOwnerSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.RESOURCE_OWNER, [RouteResourceTypes.CREATE]); - const authorizationSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.AUTHORIZED, [RouteResourceTypes.CREATE, ALWAYS]); - - if(authorizationSecurity && !authorizationSecurity.callback(req)) { - responseError(req, res, new UnauthorizedError(), 401) - return; - } - - const modelInstance = new options.resource(req.body); - - /** - * When a resourceOwnerSecurity is defined, we need to set the record that is owned by the user - */ - if(resourceOwnerSecurity) { - - if(!authorizationSecurity) { - responseError(req, res, new MissingSecurityError('Expected authorized security for this route, recieved: ' + typeof authorizationSecurity), 401); - } - - const propertyKey = resourceOwnerSecurity.arguements?.key; - const userId = App.container('requestContext').getByRequest(req, 'userId'); - - if(typeof propertyKey !== 'string') { - throw new Error('Malformed resourceOwner security. Expected parameter \'key\' to be a string but received ' + typeof propertyKey); - } - - if(!userId) { - responseError(req, res, new UnauthorizedError(), 401) - return; - } - - modelInstance.setAttribute(propertyKey, userId) - } - - await modelInstance.save(); - - res.status(201).send(modelInstance.getData({ excludeGuarded: true }) as IRouteResourceOptions['resource']) + const resourceCreateService = new ResourceCreateService(); + resourceCreateService.handler(req, res, options); } catch (err) { - if (err instanceof Error) { - responseError(req, res, err) - return; - } - - res.status(500).send({ error: 'Something went wrong' }) + ResourceErrorService.handleError(req, res, err) } } \ No newline at end of file diff --git a/src/core/domains/express/services/Resources/BaseResourceService.ts b/src/core/domains/express/services/Resources/BaseResourceService.ts index 83accd955..7d81ebe7e 100644 --- a/src/core/domains/express/services/Resources/BaseResourceService.ts +++ b/src/core/domains/express/services/Resources/BaseResourceService.ts @@ -1,7 +1,8 @@ import { Response } from "express"; -import { IResourceService } from "../../interfaces/IResourceService"; +import { IPartialRouteResourceOptions, IResourceService } from "../../interfaces/IResourceService"; import { IRouteResourceOptions } from "../../interfaces/IRouteResourceOptions"; +import { IIdentifiableSecurityCallback } from "../../interfaces/ISecurity"; import { RouteResourceTypes } from "../../routing/RouteResource"; import { BaseRequest } from "../../types/BaseRequest.t"; import { ALWAYS } from "../Security"; @@ -10,6 +11,11 @@ import { SecurityIdentifiers } from "../SecurityRules"; abstract class BaseResourceService implements IResourceService { + /** + * The route resource type (RouteResourceTypes) + */ + abstract routeResourceType: string; + // eslint-disable-next-line no-unused-vars abstract handler(req: BaseRequest, res: Response, options: IRouteResourceOptions): Promise; @@ -20,8 +26,8 @@ abstract class BaseResourceService implements IResourceService { * @param {IRouteResourceOptions} options - The options object * @returns {boolean} - Whether the request is authorized and resource owner security is set */ - validateResourceOwner(req: BaseRequest, options: IRouteResourceOptions): boolean { - const resourceOwnerSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.RESOURCE_OWNER, [RouteResourceTypes.ALL]) + validateResourceOwner(req: BaseRequest, options: IRouteResourceOptions, when: string[] = [this.routeResourceType]): boolean { + const resourceOwnerSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.RESOURCE_OWNER, when) if(this.validateAuthorization(req, options) && resourceOwnerSecurity ) { return true; @@ -47,6 +53,15 @@ abstract class BaseResourceService implements IResourceService { return true; } + /** + * Finds the resource owner security from the given options + * @param {IRouteResourceOptions} options - The options object + * @returns {IIdentifiableSecurityCallback | undefined} - The found resource owner security or undefined if not found + */ + getResourceOwnerSecurity(options: IPartialRouteResourceOptions): IIdentifiableSecurityCallback | undefined { + return SecurityReader.findFromRouteResourceOptions(options as IRouteResourceOptions, SecurityIdentifiers.RESOURCE_OWNER, [this.routeResourceType]); + } + } export default BaseResourceService \ No newline at end of file diff --git a/src/core/domains/express/services/Resources/ResourceAllService.ts b/src/core/domains/express/services/Resources/ResourceAllService.ts index d3e068232..b22c56d88 100644 --- a/src/core/domains/express/services/Resources/ResourceAllService.ts +++ b/src/core/domains/express/services/Resources/ResourceAllService.ts @@ -10,13 +10,13 @@ import { RouteResourceTypes } from "../../routing/RouteResource"; import { BaseRequest } from "../../types/BaseRequest.t"; import stripGuardedResourceProperties from "../../utils/stripGuardedResourceProperties"; import Paginate from "../Paginate"; -import SecurityReader from "../SecurityReader"; -import { SecurityIdentifiers } from "../SecurityRules"; import BaseResourceService from "./BaseResourceService"; class ResourceAllService extends BaseResourceService { + routeResourceType: string = RouteResourceTypes.ALL + /** * Handles the resource all action * - Validates that the request is authorized @@ -43,7 +43,7 @@ class ResourceAllService extends BaseResourceService { // Check if the resource owner security applies to this route and it is valid // If it is valid, we add the owner's id to the filters if(this.validateResourceOwner(req, options)) { - const resourceOwnerSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.RESOURCE_OWNER, [RouteResourceTypes.ALL]) + const resourceOwnerSecurity = this.getResourceOwnerSecurity(options) const propertyKey = resourceOwnerSecurity?.arguements?.key as string; const userId = App.container('requestContext').getByRequest(req, 'userId'); diff --git a/src/core/domains/express/services/Resources/ResourceCreateService.ts b/src/core/domains/express/services/Resources/ResourceCreateService.ts new file mode 100644 index 000000000..3251c0140 --- /dev/null +++ b/src/core/domains/express/services/Resources/ResourceCreateService.ts @@ -0,0 +1,65 @@ +import ForbiddenResourceError from "@src/core/domains/auth/exceptions/ForbiddenResourceError"; +import UnauthorizedError from "@src/core/domains/auth/exceptions/UnauthorizedError"; +import { App } from "@src/core/services/App"; +import { Response } from "express"; + +import { IRouteResourceOptions } from "../../interfaces/IRouteResourceOptions"; +import { RouteResourceTypes } from "../../routing/RouteResource"; +import { BaseRequest } from "../../types/BaseRequest.t"; +import stripGuardedResourceProperties from "../../utils/stripGuardedResourceProperties"; +import BaseResourceService from "./BaseResourceService"; + + +class ResourceCreateService extends BaseResourceService { + + routeResourceType: string = RouteResourceTypes.CREATE + + /** + * Handles the resource create action + * - Validates that the request is authorized + * - If the resource owner security is enabled, adds the owner's id to the model properties + * - Creates a new model instance with the request body + * - Saves the model instance + * - Strips the guarded properties from the model instance + * - Sends the model instance back to the client + * @param req The request object + * @param res The response object + * @param options The resource options + */ + async handler(req: BaseRequest, res: Response, options: IRouteResourceOptions): Promise { + + // Check if the authorization security applies to this route and it is valid + if(!this.validateAuthorization(req, options)) { + throw new UnauthorizedError() + } + + // Build the page options, filters + const modalInstance = new options.resource(req.body); + + // Check if the resource owner security applies to this route and it is valid + // If it is valid, we add the owner's id to the filters + if(this.validateResourceOwner(req, options)) { + const resourceOwnerSecurity = this.getResourceOwnerSecurity(options) + const propertyKey = resourceOwnerSecurity?.arguements?.key as string; + const userId = App.container('requestContext').getByRequest(req, 'userId'); + + if(!userId) { + throw new ForbiddenResourceError() + } + + if(typeof propertyKey !== 'string') { + throw new Error('Malformed resourceOwner security. Expected parameter \'key\' to be a string but received ' + typeof propertyKey); + } + + modalInstance.setAttribute(propertyKey, userId) + } + + await modalInstance.save(); + + // Send the results + res.status(201).send(stripGuardedResourceProperties(modalInstance)) + } + +} + +export default ResourceCreateService; \ No newline at end of file diff --git a/src/core/domains/express/services/Resources/ResourceShowService.ts b/src/core/domains/express/services/Resources/ResourceShowService.ts index 3ef211681..bfe936fb7 100644 --- a/src/core/domains/express/services/Resources/ResourceShowService.ts +++ b/src/core/domains/express/services/Resources/ResourceShowService.ts @@ -5,19 +5,18 @@ import { IModel } from "@src/core/interfaces/IModel"; import { App } from "@src/core/services/App"; import { Response } from "express"; -import { IPageOptions, IResourceService } from "../../interfaces/IResourceService"; +import { IPageOptions } from "../../interfaces/IResourceService"; import { IRouteResourceOptions } from "../../interfaces/IRouteResourceOptions"; import { RouteResourceTypes } from "../../routing/RouteResource"; import { BaseRequest } from "../../types/BaseRequest.t"; import stripGuardedResourceProperties from "../../utils/stripGuardedResourceProperties"; -import { ALWAYS } from "../Security"; -import SecurityReader from "../SecurityReader"; -import { SecurityIdentifiers } from "../SecurityRules"; +import BaseResourceService from "./BaseResourceService"; -class ResourceShowService implements IResourceService { +class ResourceShowService extends BaseResourceService { + + routeResourceType: string = RouteResourceTypes.SHOW - /** * Handles the resource show action * - Validates that the request is authorized @@ -43,7 +42,7 @@ class ResourceShowService implements IResourceService { // Check if the resource owner security applies to this route and it is valid // If it is valid, we add the owner's id to the filters if(this.validateResourceOwner(req, options)) { - const resourceOwnerSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.RESOURCE_OWNER, [RouteResourceTypes.ALL]) + const resourceOwnerSecurity = this.getResourceOwnerSecurity(options) const propertyKey = resourceOwnerSecurity?.arguements?.key as string; const userId = App.container('requestContext').getByRequest(req, 'userId'); @@ -90,40 +89,6 @@ class ResourceShowService implements IResourceService { }) } - /** - * Checks if the request is authorized to perform the action and if the resource owner security is set - * - * @param {BaseRequest} req - The request object - * @param {IRouteResourceOptions} options - The options object - * @returns {boolean} - Whether the request is authorized and resource owner security is set - */ - validateResourceOwner(req: BaseRequest, options: IRouteResourceOptions): boolean { - const resourceOwnerSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.RESOURCE_OWNER, [RouteResourceTypes.ALL]) - - if(this.validateAuthorization(req, options) && resourceOwnerSecurity ) { - return true; - } - - return false; - } - - /** - * Checks if the request is authorized to perform the action - * - * @param {BaseRequest} req - The request object - * @param {IRouteResourceOptions} options - The options object - * @returns {boolean} - Whether the request is authorized - */ - validateAuthorization(req: BaseRequest, options: IRouteResourceOptions): boolean { - const authorizationSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.AUTHORIZED, [RouteResourceTypes.ALL, ALWAYS]); - - if(authorizationSecurity && !authorizationSecurity.callback(req)) { - return false; - } - - return true; - } - /** * Builds the filters object * From e0c62b769e8707898a1d2801471466a976700b03 Mon Sep 17 00:00:00 2001 From: "BENJAMIN\\bensh" Date: Sun, 6 Oct 2024 13:49:39 +0100 Subject: [PATCH 7/9] refactor(express): Refactored resource delete into a service --- .../domains/express/actions/resourceDelete.ts | 48 +++-------------- .../services/Resources/BaseResourceService.ts | 19 +++++++ .../Resources/ResourceDeleteService.ts | 54 +++++++++++++++++++ 3 files changed, 79 insertions(+), 42 deletions(-) create mode 100644 src/core/domains/express/services/Resources/ResourceDeleteService.ts diff --git a/src/core/domains/express/actions/resourceDelete.ts b/src/core/domains/express/actions/resourceDelete.ts index a042ec504..49985dcbf 100644 --- a/src/core/domains/express/actions/resourceDelete.ts +++ b/src/core/domains/express/actions/resourceDelete.ts @@ -1,16 +1,10 @@ -import Repository from '@src/core/base/Repository'; -import ForbiddenResourceError from '@src/core/domains/auth/exceptions/ForbiddenResourceError'; -import UnauthorizedError from '@src/core/domains/auth/exceptions/UnauthorizedError'; import { IRouteResourceOptions } from '@src/core/domains/express/interfaces/IRouteResourceOptions'; -import responseError from '@src/core/domains/express/requests/responseError'; -import { RouteResourceTypes } from '@src/core/domains/express/routing/RouteResource'; -import { ALWAYS } from '@src/core/domains/express/services/Security'; -import SecurityReader from '@src/core/domains/express/services/SecurityReader'; -import { SecurityIdentifiers } from '@src/core/domains/express/services/SecurityRules'; import { BaseRequest } from "@src/core/domains/express/types/BaseRequest.t"; -import ModelNotFound from '@src/core/exceptions/ModelNotFound'; import { Response } from 'express'; +import ResourceDeleteService from '../services/Resources/ResourceDeleteService'; +import ResourceErrorService from '../services/Resources/ResourceErrorService'; + /** * Deletes a resource * @@ -21,40 +15,10 @@ import { Response } from 'express'; */ export default async (req: BaseRequest, res: Response, options: IRouteResourceOptions): Promise => { try { - const resourceOwnerSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.RESOURCE_OWNER, [RouteResourceTypes.DESTROY]); - const authorizationSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.AUTHORIZED, [RouteResourceTypes.DESTROY, ALWAYS]); - - if(authorizationSecurity && !authorizationSecurity.callback(req)) { - responseError(req, res, new UnauthorizedError(), 401) - return; - } - const repository = new Repository(options.resource); - - const result = await repository.findById(req.params?.id); - - if (!result) { - throw new ModelNotFound('Resource not found'); - } - - if(resourceOwnerSecurity && !resourceOwnerSecurity.callback(req, result)) { - responseError(req, res, new ForbiddenResourceError(), 403) - return; - } - - await result.delete(); - - res.send({ success: true }) + const resourceDeleteService = new ResourceDeleteService(); + resourceDeleteService.handler(req, res, options); } catch (err) { - if(err instanceof ModelNotFound) { - responseError(req, res, err, 404) - return; - } - if (err instanceof Error) { - responseError(req, res, err) - return; - } - - res.status(500).send({ error: 'Something went wrong' }) + ResourceErrorService.handleError(req, res, err) } } \ No newline at end of file diff --git a/src/core/domains/express/services/Resources/BaseResourceService.ts b/src/core/domains/express/services/Resources/BaseResourceService.ts index 7d81ebe7e..4cbfdd9ce 100644 --- a/src/core/domains/express/services/Resources/BaseResourceService.ts +++ b/src/core/domains/express/services/Resources/BaseResourceService.ts @@ -1,3 +1,4 @@ +import { IModel } from "@src/core/interfaces/IModel"; import { Response } from "express"; import { IPartialRouteResourceOptions, IResourceService } from "../../interfaces/IResourceService"; @@ -35,6 +36,24 @@ abstract class BaseResourceService implements IResourceService { return false; } + + /** + * Checks if the request is authorized to perform the action and if the resource owner security is set on the given resource instance + * + * @param {BaseRequest} req - The request object + * @param {IRouteResourceOptions} options - The options object + * @param {IModel} resourceInstance - The resource instance + * @returns {boolean} - Whether the request is authorized and resource owner security is set on the given resource instance + */ + validateResourceOwnerCallback(req: BaseRequest, options: IRouteResourceOptions, resourceInstance: IModel): boolean { + const resourceOwnerSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.RESOURCE_OWNER, [this.routeResourceType]) + + if(this.validateAuthorization(req, options) && resourceOwnerSecurity?.callback(req, resourceInstance)) { + return true; + } + + return false; + } /** * Checks if the request is authorized to perform the action diff --git a/src/core/domains/express/services/Resources/ResourceDeleteService.ts b/src/core/domains/express/services/Resources/ResourceDeleteService.ts new file mode 100644 index 000000000..1d4ad0632 --- /dev/null +++ b/src/core/domains/express/services/Resources/ResourceDeleteService.ts @@ -0,0 +1,54 @@ +import Repository from "@src/core/base/Repository"; +import ForbiddenResourceError from "@src/core/domains/auth/exceptions/ForbiddenResourceError"; +import UnauthorizedError from "@src/core/domains/auth/exceptions/UnauthorizedError"; +import ModelNotFound from "@src/core/exceptions/ModelNotFound"; +import { Response } from "express"; + +import { IRouteResourceOptions } from "../../interfaces/IRouteResourceOptions"; +import { RouteResourceTypes } from "../../routing/RouteResource"; +import { BaseRequest } from "../../types/BaseRequest.t"; +import BaseResourceService from "./BaseResourceService"; + + +class ResourceDeleteService extends BaseResourceService { + + routeResourceType: string = RouteResourceTypes.DESTROY + + /** + * Handles the resource delete action + * - Validates that the request is authorized + * - Checks if the resource owner security applies to this route and it is valid + * - Deletes the resource + * - Sends the results back to the client + * @param {BaseRequest} req - The request object + * @param {Response} res - The response object + * @param {IRouteResourceOptions} options - The options object + * @returns {Promise} + */ + async handler(req: BaseRequest, res: Response, options: IRouteResourceOptions): Promise { + + // Check if the authorization security applies to this route and it is valid + if(!this.validateAuthorization(req, options)) { + throw new UnauthorizedError() + } + + const repository = new Repository(options.resource) + + const result = await repository.findById(req.params?.id) + + if(!result) { + throw new ModelNotFound() + } + + // Check if the resource owner security applies to this route and it is valid + if(this.validateResourceOwnerCallback(req, options, result)) { + throw new ForbiddenResourceError() + } + + // Send the results + res.send({ success: true }) + } + +} + +export default ResourceDeleteService; \ No newline at end of file From d2fa72f6085b3bd0651abebf582bbce63c1a9074 Mon Sep 17 00:00:00 2001 From: "BENJAMIN\\bensh" Date: Sun, 6 Oct 2024 20:20:25 +0100 Subject: [PATCH 8/9] refactor(express): Refactored resource update into its own service, added awaits to all resource service handlers --- .../domains/express/actions/resourceAll.ts | 8 +-- .../domains/express/actions/resourceCreate.ts | 7 +-- .../domains/express/actions/resourceDelete.ts | 7 +-- .../domains/express/actions/resourceShow.ts | 7 +-- .../domains/express/actions/resourceUpdate.ts | 54 ++---------------- .../express/interfaces/IResourceService.ts | 5 +- .../services/Resources/BaseResourceService.ts | 23 ++++---- .../services/Resources/ResourceAllService.ts | 15 +++-- .../Resources/ResourceCreateService.ts | 11 ++-- .../Resources/ResourceDeleteService.ts | 11 ++-- .../Resources/ResourceErrorService.ts | 15 +---- .../services/Resources/ResourceShowService.ts | 13 ++--- .../Resources/ResourceUpdateService.ts | 57 +++++++++++++++++++ 13 files changed, 113 insertions(+), 120 deletions(-) create mode 100644 src/core/domains/express/services/Resources/ResourceUpdateService.ts diff --git a/src/core/domains/express/actions/resourceAll.ts b/src/core/domains/express/actions/resourceAll.ts index c1d872fcb..36f5d99e3 100644 --- a/src/core/domains/express/actions/resourceAll.ts +++ b/src/core/domains/express/actions/resourceAll.ts @@ -1,11 +1,9 @@ import { IRouteResourceOptions } from '@src/core/domains/express/interfaces/IRouteResourceOptions'; +import ResourceAllService from '@src/core/domains/express/services/Resources/ResourceAllService'; +import ResourceErrorService from '@src/core/domains/express/services/Resources/ResourceErrorService'; import { BaseRequest } from "@src/core/domains/express/types/BaseRequest.t"; import { Response } from 'express'; -import ResourceAllService from '../services/Resources/ResourceAllService'; -import ResourceErrorService from '../services/Resources/ResourceErrorService'; - - /** * Finds all records in the resource's repository * @@ -17,7 +15,7 @@ import ResourceErrorService from '../services/Resources/ResourceErrorService'; export default async (req: BaseRequest, res: Response, options: IRouteResourceOptions): Promise => { try { const resourceAllService = new ResourceAllService(); - resourceAllService.handler(req, res, options); + await resourceAllService.handler(req, res, options); } catch (err) { ResourceErrorService.handleError(req, res, err) diff --git a/src/core/domains/express/actions/resourceCreate.ts b/src/core/domains/express/actions/resourceCreate.ts index bc8e38e99..22549134a 100644 --- a/src/core/domains/express/actions/resourceCreate.ts +++ b/src/core/domains/express/actions/resourceCreate.ts @@ -1,10 +1,9 @@ import { IRouteResourceOptions } from '@src/core/domains/express/interfaces/IRouteResourceOptions'; +import ResourceCreateService from '@src/core/domains/express/services/Resources/ResourceCreateService'; +import ResourceErrorService from '@src/core/domains/express/services/Resources/ResourceErrorService'; import { BaseRequest } from "@src/core/domains/express/types/BaseRequest.t"; import { Response } from 'express'; -import ResourceCreateService from '../services/Resources/ResourceCreateService'; -import ResourceErrorService from '../services/Resources/ResourceErrorService'; - /** * Creates a new instance of the model @@ -17,7 +16,7 @@ import ResourceErrorService from '../services/Resources/ResourceErrorService'; export default async (req: BaseRequest, res: Response, options: IRouteResourceOptions): Promise => { try { const resourceCreateService = new ResourceCreateService(); - resourceCreateService.handler(req, res, options); + await resourceCreateService.handler(req, res, options); } catch (err) { ResourceErrorService.handleError(req, res, err) diff --git a/src/core/domains/express/actions/resourceDelete.ts b/src/core/domains/express/actions/resourceDelete.ts index 49985dcbf..f90a47f66 100644 --- a/src/core/domains/express/actions/resourceDelete.ts +++ b/src/core/domains/express/actions/resourceDelete.ts @@ -1,10 +1,9 @@ import { IRouteResourceOptions } from '@src/core/domains/express/interfaces/IRouteResourceOptions'; +import ResourceDeleteService from '@src/core/domains/express/services/Resources/ResourceDeleteService'; +import ResourceErrorService from '@src/core/domains/express/services/Resources/ResourceErrorService'; import { BaseRequest } from "@src/core/domains/express/types/BaseRequest.t"; import { Response } from 'express'; -import ResourceDeleteService from '../services/Resources/ResourceDeleteService'; -import ResourceErrorService from '../services/Resources/ResourceErrorService'; - /** * Deletes a resource * @@ -16,7 +15,7 @@ import ResourceErrorService from '../services/Resources/ResourceErrorService'; export default async (req: BaseRequest, res: Response, options: IRouteResourceOptions): Promise => { try { const resourceDeleteService = new ResourceDeleteService(); - resourceDeleteService.handler(req, res, options); + await resourceDeleteService.handler(req, res, options); } catch (err) { ResourceErrorService.handleError(req, res, err) diff --git a/src/core/domains/express/actions/resourceShow.ts b/src/core/domains/express/actions/resourceShow.ts index abe8d5927..3b4a8abd2 100644 --- a/src/core/domains/express/actions/resourceShow.ts +++ b/src/core/domains/express/actions/resourceShow.ts @@ -1,10 +1,9 @@ import { IRouteResourceOptions } from '@src/core/domains/express/interfaces/IRouteResourceOptions'; +import ResourceErrorService from '@src/core/domains/express/services/Resources/ResourceErrorService'; +import ResourceShowService from '@src/core/domains/express/services/Resources/ResourceShowService'; import { BaseRequest } from "@src/core/domains/express/types/BaseRequest.t"; import { Response } from 'express'; -import ResourceErrorService from '../services/Resources/ResourceErrorService'; -import ResourceShowService from '../services/Resources/ResourceShowService'; - /** * Finds a resource by id * @@ -16,7 +15,7 @@ import ResourceShowService from '../services/Resources/ResourceShowService'; export default async (req: BaseRequest, res: Response, options: IRouteResourceOptions): Promise => { try { const resourceShowService = new ResourceShowService() - resourceShowService.handler(req, res, options) + await resourceShowService.handler(req, res, options) } catch (err) { ResourceErrorService.handleError(req, res, err) diff --git a/src/core/domains/express/actions/resourceUpdate.ts b/src/core/domains/express/actions/resourceUpdate.ts index ce9f9a0a0..14b3bc050 100644 --- a/src/core/domains/express/actions/resourceUpdate.ts +++ b/src/core/domains/express/actions/resourceUpdate.ts @@ -1,16 +1,7 @@ -import Repository from '@src/core/base/Repository'; -import ForbiddenResourceError from '@src/core/domains/auth/exceptions/ForbiddenResourceError'; -import UnauthorizedError from '@src/core/domains/auth/exceptions/UnauthorizedError'; -import MissingSecurityError from '@src/core/domains/express/exceptions/MissingSecurityError'; import { IRouteResourceOptions } from '@src/core/domains/express/interfaces/IRouteResourceOptions'; -import responseError from '@src/core/domains/express/requests/responseError'; -import { RouteResourceTypes } from '@src/core/domains/express/routing/RouteResource'; -import { ALWAYS } from '@src/core/domains/express/services/Security'; -import SecurityReader from '@src/core/domains/express/services/SecurityReader'; -import { SecurityIdentifiers } from '@src/core/domains/express/services/SecurityRules'; +import ResourceErrorService from '@src/core/domains/express/services/Resources/ResourceErrorService'; +import ResourceUpdateService from '@src/core/domains/express/services/Resources/ResourceUpdateService'; import { BaseRequest } from "@src/core/domains/express/types/BaseRequest.t"; -import ModelNotFound from '@src/core/exceptions/ModelNotFound'; -import { IModel } from '@src/core/interfaces/IModel'; import { Response } from 'express'; /** @@ -23,45 +14,10 @@ import { Response } from 'express'; */ export default async (req: BaseRequest, res: Response, options: IRouteResourceOptions): Promise => { try { - const resourceOwnerSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.RESOURCE_OWNER, [RouteResourceTypes.UPDATE]); - const authorizationSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.AUTHORIZED, [RouteResourceTypes.UPDATE, ALWAYS]); - - if(authorizationSecurity && !authorizationSecurity.callback(req)) { - responseError(req, res, new UnauthorizedError(), 401) - return; - } - const repository = new Repository(options.resource); - - const result = await repository.findById(req.params?.id); - - if (!result) { - throw new ModelNotFound('Resource not found'); - } - - if(resourceOwnerSecurity && !authorizationSecurity) { - responseError(req, res, new MissingSecurityError('Expected authorized security for this route, recieved: ' + typeof authorizationSecurity), 401); - } - - if(resourceOwnerSecurity && !resourceOwnerSecurity.callback(req, result)) { - responseError(req, res, new ForbiddenResourceError(), 403) - return; - } - - result.fill(req.body); - await result.save(); - - res.send(result?.getData({ excludeGuarded: true }) as IModel); + const resourceUpdateService = new ResourceUpdateService(); + await resourceUpdateService.handler(req, res, options); } catch (err) { - if(err instanceof ModelNotFound) { - responseError(req, res, err, 404) - return; - } - if (err instanceof Error) { - responseError(req, res, err) - return; - } - - res.status(500).send({ error: 'Something went wrong' }) + ResourceErrorService.handleError(req, res, err) } } \ No newline at end of file diff --git a/src/core/domains/express/interfaces/IResourceService.ts b/src/core/domains/express/interfaces/IResourceService.ts index 89dc36918..3bd52c8dd 100644 --- a/src/core/domains/express/interfaces/IResourceService.ts +++ b/src/core/domains/express/interfaces/IResourceService.ts @@ -1,9 +1,8 @@ /* eslint-disable no-unused-vars */ +import { IRouteResourceOptions } from "@src/core/domains/express/interfaces/IRouteResourceOptions"; +import { BaseRequest } from "@src/core/domains/express/types/BaseRequest.t"; import { Response } from "express"; -import { BaseRequest } from "../types/BaseRequest.t"; -import { IRouteResourceOptions } from "./IRouteResourceOptions"; - export type IPartialRouteResourceOptions = Omit export interface IResourceService { diff --git a/src/core/domains/express/services/Resources/BaseResourceService.ts b/src/core/domains/express/services/Resources/BaseResourceService.ts index 4cbfdd9ce..0bb818d85 100644 --- a/src/core/domains/express/services/Resources/BaseResourceService.ts +++ b/src/core/domains/express/services/Resources/BaseResourceService.ts @@ -1,15 +1,14 @@ +import { IPartialRouteResourceOptions, IResourceService } from "@src/core/domains/express/interfaces/IResourceService"; +import { IRouteResourceOptions } from "@src/core/domains/express/interfaces/IRouteResourceOptions"; +import { IIdentifiableSecurityCallback } from "@src/core/domains/express/interfaces/ISecurity"; +import { RouteResourceTypes } from "@src/core/domains/express/routing/RouteResource"; +import { ALWAYS } from "@src/core/domains/express/services/Security"; +import SecurityReader from "@src/core/domains/express/services/SecurityReader"; +import { SecurityIdentifiers } from "@src/core/domains/express/services/SecurityRules"; +import { BaseRequest } from "@src/core/domains/express/types/BaseRequest.t"; import { IModel } from "@src/core/interfaces/IModel"; import { Response } from "express"; -import { IPartialRouteResourceOptions, IResourceService } from "../../interfaces/IResourceService"; -import { IRouteResourceOptions } from "../../interfaces/IRouteResourceOptions"; -import { IIdentifiableSecurityCallback } from "../../interfaces/ISecurity"; -import { RouteResourceTypes } from "../../routing/RouteResource"; -import { BaseRequest } from "../../types/BaseRequest.t"; -import { ALWAYS } from "../Security"; -import SecurityReader from "../SecurityReader"; -import { SecurityIdentifiers } from "../SecurityRules"; - abstract class BaseResourceService implements IResourceService { /** @@ -27,8 +26,8 @@ abstract class BaseResourceService implements IResourceService { * @param {IRouteResourceOptions} options - The options object * @returns {boolean} - Whether the request is authorized and resource owner security is set */ - validateResourceOwner(req: BaseRequest, options: IRouteResourceOptions, when: string[] = [this.routeResourceType]): boolean { - const resourceOwnerSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.RESOURCE_OWNER, when) + validateResourceOwner(req: BaseRequest, options: IRouteResourceOptions): boolean { + const resourceOwnerSecurity = this.getResourceOwnerSecurity(options); if(this.validateAuthorization(req, options) && resourceOwnerSecurity ) { return true; @@ -46,7 +45,7 @@ abstract class BaseResourceService implements IResourceService { * @returns {boolean} - Whether the request is authorized and resource owner security is set on the given resource instance */ validateResourceOwnerCallback(req: BaseRequest, options: IRouteResourceOptions, resourceInstance: IModel): boolean { - const resourceOwnerSecurity = SecurityReader.findFromRouteResourceOptions(options, SecurityIdentifiers.RESOURCE_OWNER, [this.routeResourceType]) + const resourceOwnerSecurity = this.getResourceOwnerSecurity(options); if(this.validateAuthorization(req, options) && resourceOwnerSecurity?.callback(req, resourceInstance)) { return true; diff --git a/src/core/domains/express/services/Resources/ResourceAllService.ts b/src/core/domains/express/services/Resources/ResourceAllService.ts index b22c56d88..dbe8365a5 100644 --- a/src/core/domains/express/services/Resources/ResourceAllService.ts +++ b/src/core/domains/express/services/Resources/ResourceAllService.ts @@ -1,17 +1,16 @@ import ForbiddenResourceError from "@src/core/domains/auth/exceptions/ForbiddenResourceError"; import UnauthorizedError from "@src/core/domains/auth/exceptions/UnauthorizedError"; +import { IPageOptions } from "@src/core/domains/express/interfaces/IResourceService"; +import { IRouteResourceOptions } from "@src/core/domains/express/interfaces/IRouteResourceOptions"; +import { RouteResourceTypes } from "@src/core/domains/express/routing/RouteResource"; +import Paginate from "@src/core/domains/express/services/Paginate"; +import BaseResourceService from "@src/core/domains/express/services/Resources/BaseResourceService"; +import { BaseRequest } from "@src/core/domains/express/types/BaseRequest.t"; +import stripGuardedResourceProperties from "@src/core/domains/express/utils/stripGuardedResourceProperties"; import { IModel } from "@src/core/interfaces/IModel"; import { App } from "@src/core/services/App"; import { Response } from "express"; -import { IPageOptions } from "../../interfaces/IResourceService"; -import { IRouteResourceOptions } from "../../interfaces/IRouteResourceOptions"; -import { RouteResourceTypes } from "../../routing/RouteResource"; -import { BaseRequest } from "../../types/BaseRequest.t"; -import stripGuardedResourceProperties from "../../utils/stripGuardedResourceProperties"; -import Paginate from "../Paginate"; -import BaseResourceService from "./BaseResourceService"; - class ResourceAllService extends BaseResourceService { diff --git a/src/core/domains/express/services/Resources/ResourceCreateService.ts b/src/core/domains/express/services/Resources/ResourceCreateService.ts index 3251c0140..378a05d84 100644 --- a/src/core/domains/express/services/Resources/ResourceCreateService.ts +++ b/src/core/domains/express/services/Resources/ResourceCreateService.ts @@ -1,14 +1,13 @@ import ForbiddenResourceError from "@src/core/domains/auth/exceptions/ForbiddenResourceError"; import UnauthorizedError from "@src/core/domains/auth/exceptions/UnauthorizedError"; +import { IRouteResourceOptions } from "@src/core/domains/express/interfaces/IRouteResourceOptions"; +import { RouteResourceTypes } from "@src/core/domains/express/routing/RouteResource"; +import BaseResourceService from "@src/core/domains/express/services/Resources/BaseResourceService"; +import { BaseRequest } from "@src/core/domains/express/types/BaseRequest.t"; +import stripGuardedResourceProperties from "@src/core/domains/express/utils/stripGuardedResourceProperties"; import { App } from "@src/core/services/App"; import { Response } from "express"; -import { IRouteResourceOptions } from "../../interfaces/IRouteResourceOptions"; -import { RouteResourceTypes } from "../../routing/RouteResource"; -import { BaseRequest } from "../../types/BaseRequest.t"; -import stripGuardedResourceProperties from "../../utils/stripGuardedResourceProperties"; -import BaseResourceService from "./BaseResourceService"; - class ResourceCreateService extends BaseResourceService { diff --git a/src/core/domains/express/services/Resources/ResourceDeleteService.ts b/src/core/domains/express/services/Resources/ResourceDeleteService.ts index 1d4ad0632..7a7e49c5e 100644 --- a/src/core/domains/express/services/Resources/ResourceDeleteService.ts +++ b/src/core/domains/express/services/Resources/ResourceDeleteService.ts @@ -1,14 +1,13 @@ import Repository from "@src/core/base/Repository"; import ForbiddenResourceError from "@src/core/domains/auth/exceptions/ForbiddenResourceError"; import UnauthorizedError from "@src/core/domains/auth/exceptions/UnauthorizedError"; +import { IRouteResourceOptions } from "@src/core/domains/express/interfaces/IRouteResourceOptions"; +import { RouteResourceTypes } from "@src/core/domains/express/routing/RouteResource"; +import BaseResourceService from "@src/core/domains/express/services/Resources/BaseResourceService"; +import { BaseRequest } from "@src/core/domains/express/types/BaseRequest.t"; import ModelNotFound from "@src/core/exceptions/ModelNotFound"; import { Response } from "express"; -import { IRouteResourceOptions } from "../../interfaces/IRouteResourceOptions"; -import { RouteResourceTypes } from "../../routing/RouteResource"; -import { BaseRequest } from "../../types/BaseRequest.t"; -import BaseResourceService from "./BaseResourceService"; - class ResourceDeleteService extends BaseResourceService { @@ -41,7 +40,7 @@ class ResourceDeleteService extends BaseResourceService { } // Check if the resource owner security applies to this route and it is valid - if(this.validateResourceOwnerCallback(req, options, result)) { + if(!this.validateResourceOwnerCallback(req, options, result)) { throw new ForbiddenResourceError() } diff --git a/src/core/domains/express/services/Resources/ResourceErrorService.ts b/src/core/domains/express/services/Resources/ResourceErrorService.ts index 7b2d103c8..e8fd7cab0 100644 --- a/src/core/domains/express/services/Resources/ResourceErrorService.ts +++ b/src/core/domains/express/services/Resources/ResourceErrorService.ts @@ -1,13 +1,10 @@ -import { EnvironmentDevelopment } from "@src/core/consts/Environment"; import ForbiddenResourceError from "@src/core/domains/auth/exceptions/ForbiddenResourceError"; import UnauthorizedError from "@src/core/domains/auth/exceptions/UnauthorizedError"; +import responseError from "@src/core/domains/express/requests/responseError"; +import { BaseRequest } from "@src/core/domains/express/types/BaseRequest.t"; import ModelNotFound from "@src/core/exceptions/ModelNotFound"; -import { App } from "@src/core/services/App"; import { Response } from "express"; -import responseError from "../../requests/responseError"; -import { BaseRequest } from "../../types/BaseRequest.t"; - class ResourceErrorService { /** @@ -44,13 +41,7 @@ class ResourceErrorService { return; } - let error = 'Something went wrong.' - - if(App.env() === EnvironmentDevelopment) { - error = (err as Error)?.message ?? error - } - - res.status(500).send({ error }) + res.status(500).send({ error: 'Something went wrong.' }) } } diff --git a/src/core/domains/express/services/Resources/ResourceShowService.ts b/src/core/domains/express/services/Resources/ResourceShowService.ts index bfe936fb7..e079188b8 100644 --- a/src/core/domains/express/services/Resources/ResourceShowService.ts +++ b/src/core/domains/express/services/Resources/ResourceShowService.ts @@ -1,17 +1,16 @@ import ForbiddenResourceError from "@src/core/domains/auth/exceptions/ForbiddenResourceError"; import UnauthorizedError from "@src/core/domains/auth/exceptions/UnauthorizedError"; +import { IPageOptions } from "@src/core/domains/express/interfaces/IResourceService"; +import { IRouteResourceOptions } from "@src/core/domains/express/interfaces/IRouteResourceOptions"; +import { RouteResourceTypes } from "@src/core/domains/express/routing/RouteResource"; +import BaseResourceService from "@src/core/domains/express/services/Resources/BaseResourceService"; +import { BaseRequest } from "@src/core/domains/express/types/BaseRequest.t"; +import stripGuardedResourceProperties from "@src/core/domains/express/utils/stripGuardedResourceProperties"; import ModelNotFound from "@src/core/exceptions/ModelNotFound"; import { IModel } from "@src/core/interfaces/IModel"; import { App } from "@src/core/services/App"; import { Response } from "express"; -import { IPageOptions } from "../../interfaces/IResourceService"; -import { IRouteResourceOptions } from "../../interfaces/IRouteResourceOptions"; -import { RouteResourceTypes } from "../../routing/RouteResource"; -import { BaseRequest } from "../../types/BaseRequest.t"; -import stripGuardedResourceProperties from "../../utils/stripGuardedResourceProperties"; -import BaseResourceService from "./BaseResourceService"; - class ResourceShowService extends BaseResourceService { diff --git a/src/core/domains/express/services/Resources/ResourceUpdateService.ts b/src/core/domains/express/services/Resources/ResourceUpdateService.ts new file mode 100644 index 000000000..4aafc25bc --- /dev/null +++ b/src/core/domains/express/services/Resources/ResourceUpdateService.ts @@ -0,0 +1,57 @@ +import Repository from "@src/core/base/Repository"; +import ForbiddenResourceError from "@src/core/domains/auth/exceptions/ForbiddenResourceError"; +import UnauthorizedError from "@src/core/domains/auth/exceptions/UnauthorizedError"; +import { IRouteResourceOptions } from "@src/core/domains/express/interfaces/IRouteResourceOptions"; +import { RouteResourceTypes } from "@src/core/domains/express/routing/RouteResource"; +import BaseResourceService from "@src/core/domains/express/services/Resources/BaseResourceService"; +import { BaseRequest } from "@src/core/domains/express/types/BaseRequest.t"; +import stripGuardedResourceProperties from "@src/core/domains/express/utils/stripGuardedResourceProperties"; +import ModelNotFound from "@src/core/exceptions/ModelNotFound"; +import { Response } from "express"; + + +class ResourceUpdateService extends BaseResourceService { + + routeResourceType: string = RouteResourceTypes.UPDATE + + /** + * Handles the resource delete action + * - Validates that the request is authorized + * - Checks if the resource owner security applies to this route and it is valid + * - Deletes the resource + * - Sends the results back to the client + * @param {BaseRequest} req - The request object + * @param {Response} res - The response object + * @param {IRouteResourceOptions} options - The options object + * @returns {Promise} + */ + async handler(req: BaseRequest, res: Response, options: IRouteResourceOptions): Promise { + + // Check if the authorization security applies to this route and it is valid + if(!this.validateAuthorization(req, options)) { + throw new UnauthorizedError() + } + + const repository = new Repository(options.resource) + + const result = await repository.findById(req.params?.id) + + if (!result) { + throw new ModelNotFound(); + } + + // Check if the resource owner security applies to this route and it is valid + if(!this.validateResourceOwnerCallback(req, options, result)) { + throw new ForbiddenResourceError() + } + + result.fill(req.body); + await result.save(); + + // Send the results + res.send(stripGuardedResourceProperties(result)) + } + +} + +export default ResourceUpdateService; \ No newline at end of file From cc7c4ddb02a5ef8fb26b25d9bf54361559c49c71 Mon Sep 17 00:00:00 2001 From: "BENJAMIN\\bensh" Date: Sun, 6 Oct 2024 20:21:15 +0100 Subject: [PATCH 9/9] Removed unused import --- .../domains/express/services/Resources/ResourceShowService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/domains/express/services/Resources/ResourceShowService.ts b/src/core/domains/express/services/Resources/ResourceShowService.ts index e079188b8..6fe62dff8 100644 --- a/src/core/domains/express/services/Resources/ResourceShowService.ts +++ b/src/core/domains/express/services/Resources/ResourceShowService.ts @@ -1,6 +1,5 @@ import ForbiddenResourceError from "@src/core/domains/auth/exceptions/ForbiddenResourceError"; import UnauthorizedError from "@src/core/domains/auth/exceptions/UnauthorizedError"; -import { IPageOptions } from "@src/core/domains/express/interfaces/IResourceService"; import { IRouteResourceOptions } from "@src/core/domains/express/interfaces/IRouteResourceOptions"; import { RouteResourceTypes } from "@src/core/domains/express/routing/RouteResource"; import BaseResourceService from "@src/core/domains/express/services/Resources/BaseResourceService";