From 6e110742f946d4d0e827f4beb497ba2612568b9a Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 6 Nov 2024 14:14:05 +0100 Subject: [PATCH] feat: refactor http controller validation (#880) * feat: initial replacement of domain validation * fix: node route handlers * fix: some validator usage in route handlers * fix: validation in analysis route handlers * fix: set missing optional attributes for mounting validators * fix: minor cleanup of dependencies and unused code * refactor: http handler operation constant usage --- package-lock.json | 84 +++++--- packages/server-core/package.json | 6 +- .../analysis-bucket-file/handlers/create.ts | 57 +++--- .../analysis-bucket-file/handlers/update.ts | 15 +- .../analysis-bucket-file/utils/index.ts | 2 +- .../analysis-bucket-file/utils/validation.ts | 67 ------ .../analysis-bucket-file/utils/validator.ts | 60 ++++++ .../analysis-bucket/handlers/create.ts | 21 +- .../analysis-bucket/handlers/update.ts | 15 +- .../analysis-bucket/utils/index.ts | 2 +- .../analysis-bucket/utils/validation.ts | 55 ----- .../analysis-bucket/utils/validator.ts | 35 ++++ .../analysis-node/handlers/create.ts | 56 ++++-- .../analysis-node/handlers/update.ts | 52 +++-- .../analysis-node/utils/validation.ts | 160 ++++++--------- .../analysis-permission/handlers/create.ts | 74 ++++++- .../analysis-permission/handlers/update.ts | 43 +++- .../analysis-permission/utils/validation.ts | 139 +++---------- .../controllers/analysis/handlers/command.ts | 23 +-- .../controllers/analysis/handlers/create.ts | 35 ++-- .../controllers/analysis/handlers/update.ts | 30 ++- .../http/controllers/analysis/utils/index.ts | 3 +- .../controllers/analysis/utils/validation.ts | 90 --------- .../analysis/utils/validator-command.ts | 23 +++ .../controllers/analysis/utils/validator.ts | 60 ++++++ .../http/controllers/node/handlers/create.ts | 38 +++- .../http/controllers/node/handlers/update.ts | 40 ++-- .../http/controllers/node/utils/validation.ts | 190 ++++++++---------- .../project-node/handlers/create.ts | 35 +++- .../project-node/handlers/update.ts | 16 +- .../project-node/utils/validation.ts | 108 ++++------ .../controllers/project/handlers/create.ts | 43 +++- .../controllers/project/handlers/update.ts | 17 +- .../controllers/project/utils/validation.ts | 75 ------- .../controllers/project/utils/validator.ts | 50 +++++ .../registry-project/handlers/create.ts | 20 +- .../registry-project/handlers/index.ts | 2 +- .../registry-project/handlers/update.ts | 24 ++- .../registry-project/utils/index.ts | 2 +- .../registry-project/utils/validation.ts | 76 ------- .../registry-project/utils/validator.ts | 71 +++++++ .../controllers/registry/handlers/create.ts | 17 +- .../controllers/registry/handlers/update.ts | 19 +- .../http/controllers/registry/utils/index.ts | 2 +- .../controllers/registry/utils/validation.ts | 68 ------- .../controllers/registry/utils/validator.ts | 71 +++++++ .../service/handlers/registry/command.ts | 7 +- .../controllers/service/utils/validation.ts | 48 ++--- .../test/unit/http/analysis.spec.ts | 6 +- packages/server-core/test/utils/properties.ts | 6 + packages/server-http-kit/package.json | 5 +- .../src/{validation/index.ts => constants.ts} | 10 +- packages/server-http-kit/src/index.ts | 2 +- .../server-http-kit/src/middlewares/error.ts | 10 + .../server-http-kit/src/validation/message.ts | 17 -- .../server-http-kit/src/validation/module.ts | 49 ----- .../server-http-kit/src/validation/result.ts | 58 ------ .../server-http-kit/src/validation/type.ts | 28 --- packages/server-kit/package.json | 4 +- packages/server-storage/package.json | 6 +- .../controllers/bucket/handlers/create.ts | 28 ++- .../controllers/bucket/handlers/update.ts | 15 +- .../controllers/bucket/utils/validation.ts | 66 +++--- 63 files changed, 1249 insertions(+), 1307 deletions(-) delete mode 100644 packages/server-core/src/http/controllers/analysis-bucket-file/utils/validation.ts create mode 100644 packages/server-core/src/http/controllers/analysis-bucket-file/utils/validator.ts delete mode 100644 packages/server-core/src/http/controllers/analysis-bucket/utils/validation.ts create mode 100644 packages/server-core/src/http/controllers/analysis-bucket/utils/validator.ts delete mode 100644 packages/server-core/src/http/controllers/analysis/utils/validation.ts create mode 100644 packages/server-core/src/http/controllers/analysis/utils/validator-command.ts create mode 100644 packages/server-core/src/http/controllers/analysis/utils/validator.ts delete mode 100644 packages/server-core/src/http/controllers/project/utils/validation.ts create mode 100644 packages/server-core/src/http/controllers/project/utils/validator.ts delete mode 100644 packages/server-core/src/http/controllers/registry-project/utils/validation.ts create mode 100644 packages/server-core/src/http/controllers/registry-project/utils/validator.ts delete mode 100644 packages/server-core/src/http/controllers/registry/utils/validation.ts create mode 100644 packages/server-core/src/http/controllers/registry/utils/validator.ts rename packages/server-http-kit/src/{validation/index.ts => constants.ts} (55%) delete mode 100644 packages/server-http-kit/src/validation/message.ts delete mode 100644 packages/server-http-kit/src/validation/module.ts delete mode 100644 packages/server-http-kit/src/validation/result.ts delete mode 100644 packages/server-http-kit/src/validation/type.ts diff --git a/package-lock.json b/package-lock.json index a470315aa..64ea8a636 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7710,6 +7710,34 @@ "vue": ">=2.7 || >=3" } }, + "node_modules/@validup/adapter-routup": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@validup/adapter-routup/-/adapter-routup-0.1.8.tgz", + "integrity": "sha512-qLcqxm4YuwFWysfJThxMgEhOyz/9K3W+ZCuuCaoAsE3J5RJbJEFiMoI1aMJPp/56H2RA14Q2Md8ukksbAo5EWQ==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@routup/basic": "^1.4.1", + "routup": "^4.0.1", + "validup": "^0.1.7" + } + }, + "node_modules/@validup/adapter-validator": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@validup/adapter-validator/-/adapter-validator-0.1.7.tgz", + "integrity": "sha512-ioLFf112KRpBBKk9FKLWVEBWr5BsVPVBQ2MnY4AvYJRqZAU+wVtFkSP9Q9BeCTAxLjeSdVlAdS1VggJvuUslmg==", + "dependencies": { + "smob": "^1.5.0", + "validup": "^0.1.7" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "express-validator": "^7.2.0" + } + }, "node_modules/@validup/adapter-zod": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/@validup/adapter-zod/-/adapter-zod-0.1.7.tgz", @@ -13830,6 +13858,7 @@ "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.2.0.tgz", "integrity": "sha512-I2ByKD8panjtr8Y05l21Wph9xk7kk64UMyvJCl/fFM/3CTJq8isXYPLeKW/aZBCdb/LYNv63PwhY8khw8VWocA==", "license": "MIT", + "peer": true, "dependencies": { "lodash": "^4.17.21", "validator": "~13.12.0" @@ -18099,22 +18128,24 @@ } }, "node_modules/locter": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/locter/-/locter-2.1.4.tgz", - "integrity": "sha512-3Q+ENHAMY8X033Ak6p+UyMEm5NrWz+fwvUVuNvxcgUJMbnXA2pZokCPJU79iCH6hB95QrBpV99zjtdqYiX/HTw==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/locter/-/locter-2.1.5.tgz", + "integrity": "sha512-eI57PuVxigQ0GBscGIIFGPB467E5zKODHD3XGuknzLvf7HdnvRw3GdZVGj1J8XKsKOYovZQesX/oOdTwbdjwuQ==", + "license": "MIT", "dependencies": { "destr": "^2.0.3", "ebec": "^2.3.0", "fast-glob": "^3.3.2", "flat": "^5.0.2", - "jiti": "^2.3.3", - "yaml": "^2.5.1" + "jiti": "^2.4.0", + "yaml": "^2.6.0" } }, "node_modules/locter/node_modules/jiti": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.3.3.tgz", - "integrity": "sha512-EX4oNDwcXSivPrw2qKH2LB5PoFxEvgtv2JgwW0bU858HoLQ+kutSvjLMUqBd0PeJYEinLWhoI9Ol0eYMqj/wNQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.0.tgz", + "integrity": "sha512-H5UpaUI+aHOqZXlYOaFP/8AzKsg+guWu+Pr3Y8i7+Y3zr1aXAvCvTAQ1RxSc6oVD8R8c7brgNtTVP91E7upH/g==", + "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -25961,14 +25992,15 @@ } }, "node_modules/typeorm-extension": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/typeorm-extension/-/typeorm-extension-3.6.2.tgz", - "integrity": "sha512-ao3gWO3afJAFaj901HxEJ1y5b7Cl4N/kRwHgre5Ni/jC4FrpZhM0Pq+DLIG3uGlc0E89bL8P1NUGKyvylSYbDA==", + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/typeorm-extension/-/typeorm-extension-3.6.3.tgz", + "integrity": "sha512-AE+8KqBphlBdVz5JS77o6LZzzi+b+YFFt8So4Qu/KRo/iynAwekrx98Oxuu3FAYNm6DUKDcubOBMZsJeiRvHkA==", + "license": "MIT", "dependencies": { "@faker-js/faker": "^8.4.1", "consola": "^3.2.3", "envix": "^1.5.0", - "locter": "^2.1.3", + "locter": "^2.1.5", "pascal-case": "^3.1.2", "rapiq": "^0.9.0", "reflect-metadata": "^0.2.2", @@ -26741,6 +26773,7 @@ "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.10" } @@ -28155,9 +28188,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", - "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", + "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -28800,13 +28833,14 @@ "@routup/rate-limit": "^2.4.0", "@routup/swagger": "^2.4.1", "@socket.io/redis-emitter": "^5.1.0", + "@validup/adapter-routup": "^0.1.8", + "@validup/adapter-validator": "^0.1.7", "amqp-extension": "^4.0.0-beta.3", "busboy": "^1.6.0", "cors": "^2.8.5", "docker-scan": "^1.1.0", "dotenv": "^16.4.5", "envix": "^1.3.0", - "express-validator": "^7.2.0", "hapic": "^2.5.1", "locter": "^2.1.4", "mysql2": "^3.11.0", @@ -28819,7 +28853,8 @@ "routup": "^4.0.1", "singa": "^1.0.0", "typeorm": "^0.3.20", - "typeorm-extension": "^3.6.2", + "typeorm-extension": "^3.6.3", + "validup": "^0.1.7", "yargs": "^17.7.1", "zod": "^3.23.7" }, @@ -28878,17 +28913,16 @@ "@routup/swagger": "^2.4.1", "cors": "^2.8.5", "hapic": "^2.5.1", - "locter": "^2.1.4" + "locter": "^2.1.4", + "validup": "^0.1.7" }, "devDependencies": { "@types/cors": "^2.8.17", - "express-validator": "^7.2.0", "redis-extension": "^2.0.1", "routup": "^4.0.1", "typeorm-extension": "^3.6.2" }, "peerDependencies": { - "express-validator": "^7.2.0", "redis-extension": "^2.0.1", "routup": "^4.0.1", "typeorm-extension": "^3.6.2" @@ -28914,14 +28948,14 @@ "@hapic/vault": "^2.3.4", "amqp-extension": "^4.0.0-beta.3", "redis-extension": "^2.0.1", - "typeorm-extension": "^3.6.2" + "typeorm-extension": "^3.6.3" }, "peerDependencies": { "@authup/core-http-kit": "^1.0.0-beta.22", "@hapic/vault": "^2.3.4", "amqp-extension": "^4.0.0-beta.3", "redis-extension": "^2.0.1", - "typeorm-extension": "^3.6.2" + "typeorm-extension": "^3.6.3" } }, "packages/server-messenger": { @@ -28982,11 +29016,12 @@ "@privateaim/storage-kit": "^0.8.4", "@routup/basic": "^1.4.1", "@routup/decorators": "^3.4.1", + "@validup/adapter-routup": "^0.1.8", + "@validup/adapter-validator": "^0.1.7", "busboy": "^1.6.0", "cors": "^2.8.5", "dotenv": "^16.4.4", "envix": "^1.3.0", - "express-validator": "^7.2.0", "hapic": "^2.5.1", "locter": "^2.1.4", "minio": "^8.0.2", @@ -28996,7 +29031,8 @@ "singa": "^1.0.0", "tar-stream": "^3.1.6", "typeorm": "^0.3.20", - "typeorm-extension": "^3.6.2" + "typeorm-extension": "^3.6.3", + "validup": "^0.1.7" }, "devDependencies": { "@types/busboy": "^1.5.3", diff --git a/packages/server-core/package.json b/packages/server-core/package.json index 135a946d7..385d6c2b9 100644 --- a/packages/server-core/package.json +++ b/packages/server-core/package.json @@ -30,13 +30,14 @@ "@routup/rate-limit": "^2.4.0", "@routup/swagger": "^2.4.1", "@socket.io/redis-emitter": "^5.1.0", + "@validup/adapter-routup": "^0.1.8", + "@validup/adapter-validator": "^0.1.7", "amqp-extension": "^4.0.0-beta.3", "busboy": "^1.6.0", "cors": "^2.8.5", "docker-scan": "^1.1.0", "dotenv": "^16.4.5", "envix": "^1.3.0", - "express-validator": "^7.2.0", "hapic": "^2.5.1", "locter": "^2.1.4", "mysql2": "^3.11.0", @@ -49,7 +50,8 @@ "routup": "^4.0.1", "singa": "^1.0.0", "typeorm": "^0.3.20", - "typeorm-extension": "^3.6.2", + "typeorm-extension": "^3.6.3", + "validup": "^0.1.7", "yargs": "^17.7.1", "zod": "^3.23.7" }, diff --git a/packages/server-core/src/http/controllers/analysis-bucket-file/handlers/create.ts b/packages/server-core/src/http/controllers/analysis-bucket-file/handlers/create.ts index 9d74ebcea..ddc0db97f 100644 --- a/packages/server-core/src/http/controllers/analysis-bucket-file/handlers/create.ts +++ b/packages/server-core/src/http/controllers/analysis-bucket-file/handlers/create.ts @@ -6,32 +6,43 @@ */ import { BadRequestError } from '@ebec/http'; import { - AnalysisBucketType, AnalysisBuildStatus, AnalysisRunStatus, + AnalysisBucketType, } from '@privateaim/core-kit'; -import { useRequestIdentityOrFail } from '@privateaim/server-http-kit'; +import { HTTPHandlerOperation, useRequestIdentityOrFail } from '@privateaim/server-http-kit'; import type { Request, Response } from 'routup'; import { sendCreated } from 'routup'; -import { useDataSource } from 'typeorm-extension'; -import { AnalysisBucketFileEntity } from '../../../../domains'; -import { runAnalysisFileValidation } from '../utils'; +import { useDataSource, validateEntityJoinColumns } from 'typeorm-extension'; +import { RoutupContainerAdapter } from '@validup/adapter-routup'; +import { AnalysisBucketFileEntity, AnalysisEntity } from '../../../../domains'; +import { AnalysisBucketFileValidator } from '../utils'; export async function createAnalysisBucketFileRouteHandler(req: Request, res: Response) : Promise { - const result = await runAnalysisFileValidation(req, 'create'); - result.data.analysis_id = result.relation.bucket.analysis_id; + const validator = new AnalysisBucketFileValidator(); + const validatorAdapter = new RoutupContainerAdapter(validator); + const data = await validatorAdapter.run(req, { + group: HTTPHandlerOperation.CREATE, + }); const dataSource = await useDataSource(); + await validateEntityJoinColumns(data, { + dataSource, + entityTarget: AnalysisBucketFileEntity, + }); + + data.analysis_id = data.bucket.analysis_id; + const repository = dataSource.getRepository(AnalysisBucketFileEntity); const identity = useRequestIdentityOrFail(req); - result.data.realm_id = identity.realmId; + data.realm_id = identity.realmId; switch (identity.type) { case 'user': { - result.data.user_id = identity.id; + data.user_id = identity.id; break; } case 'robot': { - result.data.robot_id = identity.id; + data.robot_id = identity.id; break; } default: { @@ -39,24 +50,18 @@ export async function createAnalysisBucketFileRouteHandler(req: Request, res: Re } } - let entity = repository.create(result.data); + let entity = repository.create(data); - if ( - result.relation.bucket.type === AnalysisBucketType.CODE && - result.relation.analysis - ) { - if ( - result.relation.analysis.build_status && - result.relation.analysis.build_status !== AnalysisBuildStatus.FAILED - ) { - throw new BadRequestError('The analysis has already been built and can no longer be modified.'); - } + if (data.bucket.type === AnalysisBucketType.CODE) { + const analysisRepository = dataSource.getRepository(AnalysisEntity); + const analysis = await analysisRepository.findOne({ + where: { + id: data.analysis_id, + }, + }); - if ( - result.relation.analysis.run_status && - result.relation.analysis.run_status !== AnalysisRunStatus.FAILED - ) { - throw new BadRequestError('The analysis has already been started and can no longer be modified.'); + if (analysis.configuration_locked) { + throw new BadRequestError('The analysis has already been locked and can therefore no longer be modified.'); } } diff --git a/packages/server-core/src/http/controllers/analysis-bucket-file/handlers/update.ts b/packages/server-core/src/http/controllers/analysis-bucket-file/handlers/update.ts index 90eec9fca..b4d9d16a4 100644 --- a/packages/server-core/src/http/controllers/analysis-bucket-file/handlers/update.ts +++ b/packages/server-core/src/http/controllers/analysis-bucket-file/handlers/update.ts @@ -10,13 +10,20 @@ import { isRealmResourceWritable } from '@authup/core-kit'; import type { Request, Response } from 'routup'; import { sendAccepted, useRequestParam } from 'routup'; import { useDataSource } from 'typeorm-extension'; -import { useRequestIdentityRealm } from '@privateaim/server-http-kit'; +import { HTTPHandlerOperation, useRequestIdentityRealm } from '@privateaim/server-http-kit'; +import { RoutupContainerAdapter } from '@validup/adapter-routup'; import { AnalysisBucketFileEntity } from '../../../../domains'; -import { runAnalysisFileValidation } from '../utils'; +import { AnalysisBucketFileValidator } from '../utils'; export async function updateAnalysisBucketFileRouteHandler(req: Request, res: Response) : Promise { const id = useRequestParam(req, 'id'); + const validator = new AnalysisBucketFileValidator(); + const validatorAdapter = new RoutupContainerAdapter(validator); + const data = await validatorAdapter.run(req, { + group: HTTPHandlerOperation.UPDATE, + }); + const dataSource = await useDataSource(); const repository = dataSource.getRepository(AnalysisBucketFileEntity); let entity = await repository.findOneBy({ id }); @@ -29,9 +36,7 @@ export async function updateAnalysisBucketFileRouteHandler(req: Request, res: Re throw new ForbiddenError(); } - const result = await runAnalysisFileValidation(req, 'update'); - - entity = repository.merge(entity, result.data); + entity = repository.merge(entity, data); entity = await repository.save(entity); diff --git a/packages/server-core/src/http/controllers/analysis-bucket-file/utils/index.ts b/packages/server-core/src/http/controllers/analysis-bucket-file/utils/index.ts index 0a6d8bd4c..390661cca 100644 --- a/packages/server-core/src/http/controllers/analysis-bucket-file/utils/index.ts +++ b/packages/server-core/src/http/controllers/analysis-bucket-file/utils/index.ts @@ -5,4 +5,4 @@ * view the LICENSE file that was distributed with this source code. */ -export * from './validation'; +export * from './validator'; diff --git a/packages/server-core/src/http/controllers/analysis-bucket-file/utils/validation.ts b/packages/server-core/src/http/controllers/analysis-bucket-file/utils/validation.ts deleted file mode 100644 index ac49b3e2f..000000000 --- a/packages/server-core/src/http/controllers/analysis-bucket-file/utils/validation.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2022-2024. - * Author Peter Placzek (tada5hi) - * For the full copyright and license information, - * view the LICENSE file that was distributed with this source code. - */ - -import { check } from 'express-validator'; -import type { Request } from 'routup'; -import type { HTTPValidationResult } from '@privateaim/server-http-kit'; -import { - createHTTPValidationResult, - extendHTTPValidationResultWithRelation, -} from '@privateaim/server-http-kit'; -import { AnalysisBucketEntity } from '../../../../domains'; -import type { AnalysisBucketFileEntity } from '../../../../domains'; - -export async function runAnalysisFileValidation( - req: Request, - operation: 'create' | 'update', -) : Promise> { - if (operation === 'create') { - await check('bucket_id') - .exists() - .isUUID() - .run(req); - } - - // ---------------------------------------------- - - const nameChain = check('name') - .exists() - .isString(); - - if (operation === 'update') { - nameChain.optional(); - } - - await nameChain.run(req); - - // ---------------------------------------------- - - if (operation === 'create') { - await check('external_id') - .exists() - .isUUID() - .run(req); - } - - await check('root') - .optional() - .toBoolean() - .isBoolean() - .default(false) - .run(req); - - const result = createHTTPValidationResult(req); - - // ---------------------------------------------- - - await extendHTTPValidationResultWithRelation(result, AnalysisBucketEntity, { - id: 'bucket_id', - entity: 'bucket', - }); - - return result; -} diff --git a/packages/server-core/src/http/controllers/analysis-bucket-file/utils/validator.ts b/packages/server-core/src/http/controllers/analysis-bucket-file/utils/validator.ts new file mode 100644 index 000000000..bcedc9d3c --- /dev/null +++ b/packages/server-core/src/http/controllers/analysis-bucket-file/utils/validator.ts @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2022-2024. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import { Container } from 'validup'; +import type { AnalysisBucketFile } from '@privateaim/core-kit'; +import { createValidator } from '@validup/adapter-validator'; +import { HTTPHandlerOperation } from '@privateaim/server-http-kit'; + +export class AnalysisBucketFileValidator extends Container { + protected initialize() { + super.initialize(); + + this.mount( + 'bucket_id', + { group: HTTPHandlerOperation.CREATE }, + createValidator((chain) => chain + .exists() + .notEmpty() + .isUUID()), + ); + + this.mount( + 'name', + { group: HTTPHandlerOperation.CREATE }, + createValidator((chain) => chain + .exists() + .isString()), + ); + + this.mount( + 'name', + { group: HTTPHandlerOperation.UPDATE, optional: true }, + createValidator((chain) => chain + .exists() + .isString() + .optional({ values: 'null' })), + ); + + this.mount( + 'external_id', + { group: HTTPHandlerOperation.CREATE }, + createValidator((chain) => chain + .exists() + .isUUID()), + ); + + this.mount( + 'root', + createValidator((chain) => chain + .optional() + .toBoolean() + .isBoolean() + .default(false)), + ); + } +} diff --git a/packages/server-core/src/http/controllers/analysis-bucket/handlers/create.ts b/packages/server-core/src/http/controllers/analysis-bucket/handlers/create.ts index 7129b9a28..8bc4faba2 100644 --- a/packages/server-core/src/http/controllers/analysis-bucket/handlers/create.ts +++ b/packages/server-core/src/http/controllers/analysis-bucket/handlers/create.ts @@ -6,17 +6,30 @@ */ import type { Request, Response } from 'routup'; import { sendCreated } from 'routup'; -import { useDataSource } from 'typeorm-extension'; +import { useDataSource, validateEntityJoinColumns } from 'typeorm-extension'; +import { RoutupContainerAdapter } from '@validup/adapter-routup'; +import { HTTPHandlerOperation } from '@privateaim/server-http-kit'; import { AnalysisBucketEntity } from '../../../../domains'; -import { runAnalysisBucketValidation } from '../utils'; +import { AnalysisBucketValidator } from '../utils'; export async function createAnalysisBucketRouteHandler(req: Request, res: Response) : Promise { - const result = await runAnalysisBucketValidation(req, 'create'); + const validator = new AnalysisBucketValidator(); + const validatorAdapter = new RoutupContainerAdapter(validator); + const data = await validatorAdapter.run(req, { + group: HTTPHandlerOperation.CREATE, + }); const dataSource = await useDataSource(); + await validateEntityJoinColumns(data, { + dataSource, + entityTarget: AnalysisBucketEntity, + }); + + data.realm_id = data.analysis.realm_id; + const repository = dataSource.getRepository(AnalysisBucketEntity); - let entity = repository.create(result.data); + let entity = repository.create(data); entity = await repository.save(entity); diff --git a/packages/server-core/src/http/controllers/analysis-bucket/handlers/update.ts b/packages/server-core/src/http/controllers/analysis-bucket/handlers/update.ts index a811b9719..3adac26b6 100644 --- a/packages/server-core/src/http/controllers/analysis-bucket/handlers/update.ts +++ b/packages/server-core/src/http/controllers/analysis-bucket/handlers/update.ts @@ -10,13 +10,20 @@ import { isRealmResourceWritable } from '@authup/core-kit'; import type { Request, Response } from 'routup'; import { sendAccepted, useRequestParam } from 'routup'; import { useDataSource } from 'typeorm-extension'; -import { useRequestIdentityRealm } from '@privateaim/server-http-kit'; +import { HTTPHandlerOperation, useRequestIdentityRealm } from '@privateaim/server-http-kit'; +import { RoutupContainerAdapter } from '@validup/adapter-routup'; import { AnalysisBucketEntity } from '../../../../domains'; -import { runAnalysisBucketValidation } from '../utils'; +import { AnalysisBucketValidator } from '../utils'; export async function updateAnalysisBucketRouteHandler(req: Request, res: Response) : Promise { const id = useRequestParam(req, 'id'); + const validator = new AnalysisBucketValidator(); + const validatorAdapter = new RoutupContainerAdapter(validator); + const data = await validatorAdapter.run(req, { + group: HTTPHandlerOperation.UPDATE, + }); + const dataSource = await useDataSource(); const repository = dataSource.getRepository(AnalysisBucketEntity); let entity = await repository.findOneBy({ id }); @@ -29,9 +36,7 @@ export async function updateAnalysisBucketRouteHandler(req: Request, res: Respon throw new ForbiddenError(); } - const result = await runAnalysisBucketValidation(req, 'update'); - - entity = repository.merge(entity, result.data); + entity = repository.merge(entity, data); entity = await repository.save(entity); diff --git a/packages/server-core/src/http/controllers/analysis-bucket/utils/index.ts b/packages/server-core/src/http/controllers/analysis-bucket/utils/index.ts index 0a6d8bd4c..390661cca 100644 --- a/packages/server-core/src/http/controllers/analysis-bucket/utils/index.ts +++ b/packages/server-core/src/http/controllers/analysis-bucket/utils/index.ts @@ -5,4 +5,4 @@ * view the LICENSE file that was distributed with this source code. */ -export * from './validation'; +export * from './validator'; diff --git a/packages/server-core/src/http/controllers/analysis-bucket/utils/validation.ts b/packages/server-core/src/http/controllers/analysis-bucket/utils/validation.ts deleted file mode 100644 index 68dede85f..000000000 --- a/packages/server-core/src/http/controllers/analysis-bucket/utils/validation.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2022-2024. - * Author Peter Placzek (tada5hi) - * For the full copyright and license information, - * view the LICENSE file that was distributed with this source code. - */ - -import { AnalysisBucketType } from '@privateaim/core-kit'; -import { check } from 'express-validator'; -import type { Request } from 'routup'; -import type { HTTPValidationResult } from '@privateaim/server-http-kit'; -import { - createHTTPValidationResult, - extendHTTPValidationResultWithRelation, -} from '@privateaim/server-http-kit'; -import type { AnalysisBucketEntity } from '../../../../domains'; -import { AnalysisEntity } from '../../../../domains'; - -export async function runAnalysisBucketValidation( - req: Request, - operation: 'create' | 'update', -) : Promise> { - if (operation === 'create') { - await check('analysis_id') - .exists() - .isUUID() - .run(req); - } - - await check('type') - .exists() - .notEmpty() - .isIn(Object.values(AnalysisBucketType)) - .run(req); - - const result = createHTTPValidationResult(req); - - // ---------------------------------------------- - - await extendHTTPValidationResultWithRelation(result, AnalysisEntity, { - id: 'analysis_id', - entity: 'analysis', - }); - - // ---------------------------------------------- - - if (operation === 'create') { - result.data.realm_id = result.relation.analysis.realm_id; - result.data.analysis_id = result.relation.analysis.id; - } - - // ---------------------------------------------- - - return result; -} diff --git a/packages/server-core/src/http/controllers/analysis-bucket/utils/validator.ts b/packages/server-core/src/http/controllers/analysis-bucket/utils/validator.ts new file mode 100644 index 000000000..8de088649 --- /dev/null +++ b/packages/server-core/src/http/controllers/analysis-bucket/utils/validator.ts @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2022-2024. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import type { AnalysisBucket } from '@privateaim/core-kit'; +import { AnalysisBucketType } from '@privateaim/core-kit'; +import { Container } from 'validup'; +import { createValidator } from '@validup/adapter-validator'; +import { HTTPHandlerOperation } from '@privateaim/server-http-kit'; + +export class AnalysisBucketValidator extends Container { + protected initialize() { + super.initialize(); + + this.mount( + 'analysis_id', + { group: HTTPHandlerOperation.CREATE }, + createValidator((chain) => chain + .exists() + .notEmpty() + .isUUID()), + ); + + this.mount( + 'type', + { group: HTTPHandlerOperation.CREATE }, + createValidator((chain) => chain + .notEmpty() + .isIn(Object.values(AnalysisBucketType))), + ); + } +} diff --git a/packages/server-core/src/http/controllers/analysis-node/handlers/create.ts b/packages/server-core/src/http/controllers/analysis-node/handlers/create.ts index 6fade0d6c..6363082a9 100644 --- a/packages/server-core/src/http/controllers/analysis-node/handlers/create.ts +++ b/packages/server-core/src/http/controllers/analysis-node/handlers/create.ts @@ -5,37 +5,58 @@ * view the LICENSE file that was distributed with this source code. */ -import { BadRequestError } from '@ebec/http'; +import { BadRequestError, NotFoundError } from '@ebec/http'; import { AnalysisNodeApprovalStatus, NodeType } from '@privateaim/core-kit'; import { PermissionName } from '@privateaim/kit'; import type { Request, Response } from 'routup'; import { sendCreated } from 'routup'; -import { useDataSource } from 'typeorm-extension'; -import { useRequestPermissionChecker } from '@privateaim/server-http-kit'; +import { useDataSource, validateEntityJoinColumns } from 'typeorm-extension'; +import { HTTPHandlerOperation, useRequestPermissionChecker } from '@privateaim/server-http-kit'; +import { RoutupContainerAdapter } from '@validup/adapter-routup'; import { useEnv } from '../../../../config'; -import { AnalysisEntity, AnalysisNodeEntity } from '../../../../domains'; -import { runAnalysisNodeValidation } from '../utils'; +import { AnalysisEntity, AnalysisNodeEntity, ProjectNodeEntity } from '../../../../domains'; +import { AnalysisNodeValidator } from '../utils'; export async function createAnalysisNodeRouteHandler(req: Request, res: Response) : Promise { const permissionChecker = useRequestPermissionChecker(req); await permissionChecker.preCheck({ name: PermissionName.ANALYSIS_UPDATE }); - const result = await runAnalysisNodeValidation(req, 'create'); - if ( - result.relation.analysis && - result.relation.analysis.configuration_locked - ) { - throw new BadRequestError('The analysis is locked right now. It is not possible to add new nodes.'); - } + const validator = new AnalysisNodeValidator(); + const validatorAdapter = new RoutupContainerAdapter(validator); + const data = await validatorAdapter.run(req, { + group: HTTPHandlerOperation.CREATE, + }); const dataSource = await useDataSource(); + await validateEntityJoinColumns(data, { + dataSource, + entityTarget: AnalysisNodeEntity, + }); + + data.analysis_realm_id = data.analysis.realm_id; + data.node_realm_id = data.node.realm_id; + + if (data.analysis.configuration_locked) { + throw new BadRequestError('The analysis has already been locked and can therefore no longer be modified.'); + } + + const projectNodeRepository = dataSource.getRepository(ProjectNodeEntity); + const projectNode = await projectNodeRepository.findOneBy({ + project_id: data.analysis.project_id, + node_id: data.node_id, + }); + + if (!projectNode) { + throw new NotFoundError('The referenced node is not part of the analysis project.'); + } + const repository = dataSource.getRepository(AnalysisNodeEntity); - let entity = repository.create(result.data); + let entity = repository.create(data); if ( useEnv('skipAnalysisApproval') || - (result.relation.node && result.relation.node.type === NodeType.AGGREGATOR) + data.node.type === NodeType.AGGREGATOR ) { entity.approval_status = AnalysisNodeApprovalStatus.APPROVED; } @@ -48,12 +69,9 @@ export async function createAnalysisNodeRouteHandler(req: Request, res: Response entity = await repository.save(entity); - result.relation.analysis.nodes += 1; + data.analysis.nodes += 1; const analysisRepository = dataSource.getRepository(AnalysisEntity); - await analysisRepository.save(result.relation.analysis); - - entity.analysis = result.relation.analysis; - entity.node = result.relation.node; + await analysisRepository.save(data.analysis); return sendCreated(res, entity); } diff --git a/packages/server-core/src/http/controllers/analysis-node/handlers/update.ts b/packages/server-core/src/http/controllers/analysis-node/handlers/update.ts index 0afaf1741..67e6dd465 100644 --- a/packages/server-core/src/http/controllers/analysis-node/handlers/update.ts +++ b/packages/server-core/src/http/controllers/analysis-node/handlers/update.ts @@ -5,32 +5,48 @@ * view the LICENSE file that was distributed with this source code. */ -import { ForbiddenError, NotFoundError } from '@ebec/http'; +import { BadRequestError, ForbiddenError, NotFoundError } from '@ebec/http'; import { isRealmResourceWritable } from '@authup/core-kit'; import { PermissionName } from '@privateaim/kit'; import type { Request, Response } from 'routup'; import { sendAccepted, useRequestParam } from 'routup'; import { useDataSource } from 'typeorm-extension'; -import { useRequestIdentityRealm, useRequestPermissionChecker } from '@privateaim/server-http-kit'; +import { HTTPHandlerOperation, useRequestIdentityRealm, useRequestPermissionChecker } from '@privateaim/server-http-kit'; +import { RoutupContainerAdapter } from '@validup/adapter-routup'; import { AnalysisNodeEntity } from '../../../../domains'; -import { runAnalysisNodeValidation } from '../utils'; +import { AnalysisNodeValidator } from '../utils'; export async function updateAnalysisNodeRouteHandler(req: Request, res: Response) : Promise { const id = useRequestParam(req, 'id'); + const validator = new AnalysisNodeValidator(); + const validatorAdapter = new RoutupContainerAdapter(validator); + const data = await validatorAdapter.run(req, { + group: HTTPHandlerOperation.UPDATE, + }); + const dataSource = await useDataSource(); const repository = dataSource.getRepository(AnalysisNodeEntity); - let entity = await repository.findOneBy({ id }); + let entity = await repository.findOne({ + where: { + id, + }, + relations: ['analysis'], + }); if (!entity) { throw new NotFoundError(); } + if (entity.analysis.configuration_locked) { + throw new BadRequestError('The analysis is locked right now. It is not possible to add new nodes.'); + } + const isAuthorityOfNode = isRealmResourceWritable(useRequestIdentityRealm(req), entity.node_realm_id); const isAuthorityOfAnalysis = isRealmResourceWritable(useRequestIdentityRealm(req), entity.analysis_realm_id); if (!isAuthorityOfNode && !isAuthorityOfAnalysis) { - throw new ForbiddenError(); + throw new ForbiddenError('You are neither part of the node nor analysis realm.'); } const permissionChecker = useRequestPermissionChecker(req); @@ -54,31 +70,31 @@ export async function updateAnalysisNodeRouteHandler(req: Request, res: Response throw new ForbiddenError(); } - const result = await runAnalysisNodeValidation(req, 'update'); - if (!isAuthorityOfNode || !canApprove) { - if (result.data.approval_status) { - delete result.data.approval_status; - } - - if (result.data.comment) { - delete result.data.comment; + if (data.approval_status || data.comment) { + throw new BadRequestError( + 'You are either no authority of the node or you don\'t have the required permissions', + ); } } if (!isAuthorityOfNode || !canUpdate) { - if (result.data.run_status) { - delete result.data.run_status; + if (data.run_status) { + throw new BadRequestError( + 'You are either no authority of the node or you don\'t have the required permissions', + ); } } if (!isAuthorityOfAnalysis || !canUpdate) { - if (result.data.index) { - delete result.data.index; + if (data.index) { + throw new BadRequestError( + 'You are either no authority of the analysis or you don\'t have the required permissions', + ); } } - entity = repository.merge(entity, result.data); + entity = repository.merge(entity, data); entity = await repository.save(entity); diff --git a/packages/server-core/src/http/controllers/analysis-node/utils/validation.ts b/packages/server-core/src/http/controllers/analysis-node/utils/validation.ts index fc7654edf..29f896077 100644 --- a/packages/server-core/src/http/controllers/analysis-node/utils/validation.ts +++ b/packages/server-core/src/http/controllers/analysis-node/utils/validation.ts @@ -5,111 +5,65 @@ * view the LICENSE file that was distributed with this source code. */ -import { check } from 'express-validator'; import { AnalysisNodeApprovalStatus, AnalysisNodeRunStatus } from '@privateaim/core-kit'; -import { BadRequestError, NotFoundError } from '@ebec/http'; -import { isRealmResourceWritable } from '@authup/core-kit'; -import type { Request } from 'routup'; -import { useDataSource } from 'typeorm-extension'; -import type { HTTPValidationResult } from '@privateaim/server-http-kit'; -import { - buildHTTPValidationErrorMessage, - createHTTPValidationResult, - extendHTTPValidationResultWithRelation, - useRequestIdentityRealm, -} from '@privateaim/server-http-kit'; -import { AnalysisEntity, NodeEntity, ProjectNodeEntity } from '../../../../domains'; +import { Container } from 'validup'; +import { createValidator } from '@validup/adapter-validator'; +import { HTTPHandlerOperation } from '@privateaim/server-http-kit'; import type { AnalysisNodeEntity } from '../../../../domains'; -export async function runAnalysisNodeValidation( - req: Request, - operation: 'create' | 'update', -) : Promise> { - if (operation === 'create') { - await check('node_id') - .exists() - .isUUID() - .run(req); - - await check('analysis_id') - .exists() - .isUUID() - .run(req); - } - - await check('run_status') - .isIn(Object.values(AnalysisNodeRunStatus)) - .optional({ values: 'null' }) - .run(req); - - await check('index') - .exists() - .isInt() - .optional() - .run(req); - - if (operation === 'update') { - await check('approval_status') - .optional({ nullable: true }) - .isIn(Object.values(AnalysisNodeApprovalStatus)) - .run(req); - - await check('comment') - .optional({ nullable: true }) - .isString() - .run(req); - } - - const result = createHTTPValidationResult(req); - - // ---------------------------------------------- - - await extendHTTPValidationResultWithRelation(result, AnalysisEntity, { - id: 'analysis_id', - entity: 'analysis', - }); - - if (result.relation.analysis) { - if (!isRealmResourceWritable(useRequestIdentityRealm(req), result.relation.analysis.realm_id)) { - throw new BadRequestError(buildHTTPValidationErrorMessage('analysis_id')); - } - - result.data.analysis_realm_id = result.relation.analysis.realm_id; - } - - await extendHTTPValidationResultWithRelation(result, NodeEntity, { - id: 'node_id', - entity: 'node', - }); - - if (result.relation.node) { - result.data.node_realm_id = result.relation.node.realm_id; - } - - if ( - result.relation.analysis && - result.relation.analysis.configuration_locked - ) { - throw new BadRequestError('The analysis is locked. No additional nodes can be assigned nor modified.'); +export class AnalysisNodeValidator extends Container { + protected initialize() { + super.initialize(); + + this.mount( + 'node_id', + { group: HTTPHandlerOperation.CREATE }, + createValidator((chain) => chain + .exists() + .notEmpty() + .isUUID()), + ); + + this.mount( + 'analysis_id', + { group: HTTPHandlerOperation.CREATE }, + createValidator((chain) => chain + .exists() + .notEmpty() + .isUUID()), + ); + + this.mount( + 'run_status', + { optional: true }, + createValidator((chain) => chain + .isIn(Object.values(AnalysisNodeRunStatus)) + .optional({ values: 'null' })), + ); + + this.mount( + 'index', + { optional: true }, + createValidator((chain) => chain + .exists() + .isInt() + .optional({ values: 'null' })), + ); + + this.mount( + 'approval_status', + { optional: true }, + createValidator((chain) => chain + .optional({ nullable: true }) + .isIn(Object.values(AnalysisNodeApprovalStatus))), + ); + + this.mount( + 'comment', + { optional: true }, + createValidator((chain) => chain + .optional({ nullable: true }) + .isString()), + ); } - - if ( - result.relation.node && - result.relation.analysis - ) { - const dataSource = await useDataSource(); - const proposalStationRepository = dataSource.getRepository(ProjectNodeEntity); - const proposalStation = await proposalStationRepository.findOneBy({ - project_id: result.relation.analysis.project_id, - node_id: result.relation.node.id, - }); - - if (!proposalStation) { - throw new NotFoundError('The referenced node is not part of the analysis project.'); - } - } - - // ---------------------------------------------- - - return result; } diff --git a/packages/server-core/src/http/controllers/analysis-permission/handlers/create.ts b/packages/server-core/src/http/controllers/analysis-permission/handlers/create.ts index 604100764..83d0bd92d 100644 --- a/packages/server-core/src/http/controllers/analysis-permission/handlers/create.ts +++ b/packages/server-core/src/http/controllers/analysis-permission/handlers/create.ts @@ -8,25 +8,83 @@ import { PermissionName } from '@privateaim/kit'; import type { Request, Response } from 'routup'; import { sendCreated } from 'routup'; -import { useDataSource } from 'typeorm-extension'; -import { useRequestPermissionChecker } from '@privateaim/server-http-kit'; +import { useDataSource, validateEntityJoinColumns } from 'typeorm-extension'; +import { HTTPHandlerOperation, useRequestPermissionChecker } from '@privateaim/server-http-kit'; +import { RoutupContainerAdapter } from '@validup/adapter-routup'; +import { isAuthupClientUsable, useAuthupClient } from '@privateaim/server-kit'; +import type { Permission } from '@authup/core-kit'; +import { isClientErrorWithStatusCode } from '@hapic/harbor'; +import { BadRequestError } from '@ebec/http'; +import { buildErrorMessageForAttributes } from 'validup'; import { AnalysisPermissionEntity } from '../../../../domains'; -import { runAnalysisPermissionValidation } from '../utils'; +import { AnalysisPermissionValidator } from '../utils'; export async function createAnalysisPermissionRouteHandler(req: Request, res: Response) : Promise { const permissionChecker = useRequestPermissionChecker(req); await permissionChecker.preCheck({ name: PermissionName.ANALYSIS_UPDATE }); - const result = await runAnalysisPermissionValidation(req, 'create'); + const validator = new AnalysisPermissionValidator(); + const validatorAdapter = new RoutupContainerAdapter(validator); + const data = await validatorAdapter.run(req, { + group: HTTPHandlerOperation.CREATE, + }); const dataSource = await useDataSource(); - const repository = dataSource.getRepository(AnalysisPermissionEntity); + await validateEntityJoinColumns(data, { + dataSource, + entityTarget: AnalysisPermissionEntity, + }); - let entity = repository.create(result.data); + data.analysis_realm_id = data.analysis.realm_id; - entity = await repository.save(entity); + if (isAuthupClientUsable()) { + const authup = useAuthupClient(); + + if (data.permission_id) { + let permission: Permission; + try { + permission = await authup.permission.getOne(data.permission_id); + + data.permission = permission; + data.permission_realm_id = permission.realm_id; + } catch (e) { + if (isClientErrorWithStatusCode(e, 404)) { + throw new BadRequestError(buildErrorMessageForAttributes(['permission_id'])); + } + + throw e; + } + } + + // todo: this is not possible right now :/ + /* + const data = buildAbilityFromPermission(permission); + const ability = useRequestEnv(req, 'abilities'); + if (!ability.has(data)) { + throw new ForbiddenError(`You don't own the permission ${data.name}`); + } + */ - entity.analysis = result.relation.analysis; + if (data.policy_id) { + try { + const policy = await authup.policy.getOne(data.policy_id); + data.policy = policy; + data.policy_id = policy.id; + } catch (e) { + if (isClientErrorWithStatusCode(e, 404)) { + throw new BadRequestError(buildErrorMessageForAttributes(['permission_id'])); + } + + throw e; + } + } + } + + const repository = dataSource.getRepository(AnalysisPermissionEntity); + + let entity = repository.create(data); + + entity = await repository.save(entity); return sendCreated(res, entity); } diff --git a/packages/server-core/src/http/controllers/analysis-permission/handlers/update.ts b/packages/server-core/src/http/controllers/analysis-permission/handlers/update.ts index f6310699f..1b719a726 100644 --- a/packages/server-core/src/http/controllers/analysis-permission/handlers/update.ts +++ b/packages/server-core/src/http/controllers/analysis-permission/handlers/update.ts @@ -5,20 +5,53 @@ * view the LICENSE file that was distributed with this source code. */ -import { ForbiddenError, NotFoundError } from '@ebec/http'; +import { BadRequestError, ForbiddenError, NotFoundError } from '@ebec/http'; import { isRealmResourceWritable } from '@authup/core-kit'; import { PermissionName } from '@privateaim/kit'; import type { Request, Response } from 'routup'; import { sendAccepted, useRequestParam } from 'routup'; import { useDataSource } from 'typeorm-extension'; -import { useRequestIdentityRealm, useRequestPermissionChecker } from '@privateaim/server-http-kit'; +import { + HTTPHandlerOperation, + useRequestIdentityRealm, + useRequestPermissionChecker, +} from '@privateaim/server-http-kit'; +import { isAuthupClientUsable, useAuthupClient } from '@privateaim/server-kit'; +import { RoutupContainerAdapter } from '@validup/adapter-routup'; +import { isClientErrorWithStatusCode } from '@hapic/harbor'; +import { buildErrorMessageForAttributes } from 'validup'; import { AnalysisPermissionEntity } from '../../../../domains'; -import { runAnalysisPermissionValidation } from '../utils'; +import { AnalysisPermissionValidator } from '../utils'; export async function updateAnalysisPermissionRouteHandler(req: Request, res: Response) : Promise { const permissionChecker = useRequestPermissionChecker(req); await permissionChecker.preCheck({ name: PermissionName.ANALYSIS_UPDATE }); + const validator = new AnalysisPermissionValidator(); + const validatorAdapter = new RoutupContainerAdapter(validator); + const data = await validatorAdapter.run(req, { + group: HTTPHandlerOperation.UPDATE, + }); + + if (isAuthupClientUsable()) { + const authup = useAuthupClient(); + + if (data.policy_id) { + try { + const policy = await authup.policy.getOne(data.policy_id); + + data.policy = policy; + data.policy_id = policy.id; + } catch (e) { + if (isClientErrorWithStatusCode(e, 404)) { + throw new BadRequestError(buildErrorMessageForAttributes(['permission_id'])); + } + + throw e; + } + } + } + const id = useRequestParam(req, 'id'); const dataSource = await useDataSource(); @@ -32,9 +65,7 @@ export async function updateAnalysisPermissionRouteHandler(req: Request, res: Re throw new ForbiddenError(); } - const result = await runAnalysisPermissionValidation(req, 'update'); - - entity = repository.merge(entity, result.data); + entity = repository.merge(entity, data); entity = await repository.save(entity); diff --git a/packages/server-core/src/http/controllers/analysis-permission/utils/validation.ts b/packages/server-core/src/http/controllers/analysis-permission/utils/validation.ts index c03325b9f..7a6352545 100644 --- a/packages/server-core/src/http/controllers/analysis-permission/utils/validation.ts +++ b/packages/server-core/src/http/controllers/analysis-permission/utils/validation.ts @@ -5,114 +5,39 @@ * view the LICENSE file that was distributed with this source code. */ -import { isClientErrorWithStatusCode } from '@hapic/harbor'; -import { isAuthupClientUsable, useAuthupClient } from '@privateaim/server-kit'; -import { check } from 'express-validator'; -import { BadRequestError } from '@ebec/http'; -import type { Permission } from '@authup/core-kit'; -import { isRealmResourceWritable } from '@authup/core-kit'; -import type { Request } from 'routup'; -import type { HTTPValidationResult } from '@privateaim/server-http-kit'; -import { - buildHTTPValidationErrorMessage, - createHTTPValidationResult, - extendHTTPValidationResultWithRelation, - useRequestIdentityRealm, -} from '@privateaim/server-http-kit'; -import { AnalysisEntity } from '../../../../domains'; +import { Container } from 'validup'; +import { createValidator } from '@validup/adapter-validator'; +import { HTTPHandlerOperation } from '@privateaim/server-http-kit'; import type { AnalysisPermissionEntity } from '../../../../domains'; -export async function runAnalysisPermissionValidation( - req: Request, - operation: 'create' | 'update', -) : Promise> { - if (operation === 'create') { - await check('analysis_id') - .exists() - .isUUID() - .run(req); - - await check('permission_id') - .exists() - .isUUID() - .run(req); - } - - await check('policy_id') - .isUUID() - .optional({ values: 'null' }) - .run(req); - - const result = createHTTPValidationResult(req); - - // ---------------------------------------------- - - await extendHTTPValidationResultWithRelation(result, AnalysisEntity, { - id: 'analysis_id', - entity: 'analysis', - }); - - if (result.relation.analysis) { - if (!isRealmResourceWritable(useRequestIdentityRealm(req), result.relation.analysis.realm_id)) { - throw new BadRequestError(buildHTTPValidationErrorMessage('analysis_id')); - } - - result.data.analysis_realm_id = result.relation.analysis.realm_id; - } - - // ---------------------------------------------- - - if (isAuthupClientUsable()) { - const authup = useAuthupClient(); - - if (result.data.permission_id) { - let permission: Permission; - try { - permission = await authup.permission.getOne(result.data.permission_id); - - result.data.permission = permission; - result.data.permission_realm_id = permission.realm_id; - - // todo: remove this when validation is reworked. - result.relation.permission = permission; - } catch (e) { - if (isClientErrorWithStatusCode(e, 404)) { - throw new BadRequestError(buildHTTPValidationErrorMessage('permission_id')); - } - - throw e; - } - } - - // todo: this is not possible right now :/ - /* - const data = buildAbilityFromPermission(permission); - const ability = useRequestEnv(req, 'abilities'); - if (!ability.has(data)) { - throw new ForbiddenError(`You don't own the permission ${data.name}`); - } - */ - - if (result.data.policy_id) { - try { - const policy = await authup.policy.getOne(result.data.policy_id); - - result.data.policy = policy; - result.data.policy_id = policy.id; - - // todo: remove this when validation is reworked. - result.relation.policy = policy; - } catch (e) { - if (isClientErrorWithStatusCode(e, 404)) { - throw new BadRequestError(buildHTTPValidationErrorMessage('permission_id')); - } - - throw e; - } - } +export class AnalysisPermissionValidator extends Container { + protected initialize() { + super.initialize(); + + this.mount( + 'permission_id', + { group: HTTPHandlerOperation.CREATE }, + createValidator((chain) => chain + .exists() + .notEmpty() + .isUUID()), + ); + + this.mount( + 'analysis_id', + { group: HTTPHandlerOperation.CREATE }, + createValidator((chain) => chain + .exists() + .notEmpty() + .isUUID()), + ); + + this.mount( + 'policy_id', + { optional: true }, + createValidator((chain) => chain + .isUUID() + .optional({ values: 'null' })), + ); } - - // ---------------------------------------------- - - return result; } diff --git a/packages/server-core/src/http/controllers/analysis/handlers/command.ts b/packages/server-core/src/http/controllers/analysis/handlers/command.ts index 82c630689..42a60c24a 100644 --- a/packages/server-core/src/http/controllers/analysis/handlers/command.ts +++ b/packages/server-core/src/http/controllers/analysis/handlers/command.ts @@ -8,8 +8,8 @@ import { isRealmResourceWritable } from '@authup/core-kit'; import { ForbiddenError, NotFoundError } from '@ebec/http'; import { AnalysisAPICommand } from '@privateaim/core-kit'; -import { HTTPValidationError, useRequestIdentityRealm } from '@privateaim/server-http-kit'; -import { check, matchedData, validationResult } from 'express-validator'; +import { HTTPHandlerOperation, useRequestIdentityRealm } from '@privateaim/server-http-kit'; +import { RoutupContainerAdapter } from '@validup/adapter-routup'; import type { Request, Response } from 'routup'; import { sendAccepted, useRequestParam } from 'routup'; import { useDataSource } from 'typeorm-extension'; @@ -22,6 +22,7 @@ import { unlockAnalysisConfiguration, } from '../../../../domains'; import { runAnalysisTearDownCommand } from '../../../../domains/analysis/commands/tear-down'; +import { AnalysisCommandValidator } from '../utils'; /** * Execute a analysis command (start, stop, build). @@ -36,17 +37,11 @@ export async function handleAnalysisCommandRouteHandler(req: Request, res: Respo throw new NotFoundError(); } - await check('command') - .exists() - .custom((command) => Object.values(AnalysisAPICommand).includes(command)) - .run(req); - - const validation = validationResult(req); - if (!validation.isEmpty()) { - throw new HTTPValidationError(validation); - } - - const validationData = matchedData(req, { includeOptionals: true }); + const validator = new AnalysisCommandValidator(); + const validatorAdapter = new RoutupContainerAdapter(validator); + const data = await validatorAdapter.run(req, { + group: HTTPHandlerOperation.CREATE, + }); const dataSource = await useDataSource(); const repository = dataSource.getRepository(AnalysisEntity); @@ -61,7 +56,7 @@ export async function handleAnalysisCommandRouteHandler(req: Request, res: Respo throw new ForbiddenError(); } - switch (validationData.command as AnalysisAPICommand) { + switch (data.command) { // General case AnalysisAPICommand.SPIN_UP: { entity = await runAnalysisSpinUpCommand(entity); diff --git a/packages/server-core/src/http/controllers/analysis/handlers/create.ts b/packages/server-core/src/http/controllers/analysis/handlers/create.ts index 553078b99..89a5cdbcd 100644 --- a/packages/server-core/src/http/controllers/analysis/handlers/create.ts +++ b/packages/server-core/src/http/controllers/analysis/handlers/create.ts @@ -10,35 +10,44 @@ import type { Analysis } from '@privateaim/core-kit'; import { PermissionName } from '@privateaim/kit'; import type { Request, Response } from 'routup'; import { sendCreated } from 'routup'; -import { useDataSource } from 'typeorm-extension'; +import { useDataSource, validateEntityJoinColumns } from 'typeorm-extension'; import { + HTTPHandlerOperation, useRequestIdentityOrFail, useRequestPermissionChecker, } from '@privateaim/server-http-kit'; -import { runAnalysisValidation } from '../utils'; +import { RoutupContainerAdapter } from '@validup/adapter-routup'; +import { AnalysisValidator } from '../utils'; import { AnalysisEntity, ProjectEntity, runAnalysisSpinUpCommand } from '../../../../domains'; export async function createAnalysisRouteHandler(req: Request, res: Response) : Promise { const permissionChecker = useRequestPermissionChecker(req); await permissionChecker.preCheck({ name: PermissionName.ANALYSIS_CREATE }); - const result = await runAnalysisValidation(req, 'create'); + const validator = new AnalysisValidator(); + const validatorAdapter = new RoutupContainerAdapter(validator); + const data = await validatorAdapter.run(req, { + group: HTTPHandlerOperation.CREATE, + }); + + const dataSource = await useDataSource(); + await validateEntityJoinColumns(data, { + dataSource, + entityTarget: AnalysisEntity, + }); - if ( - !result.data.master_image_id && - result.relation.project - ) { - result.data.master_image_id = result.relation.project.master_image_id; + if (!data.master_image_id) { + data.master_image_id = data.project.master_image_id; } - const dataSource = await useDataSource(); - const repository = dataSource.getRepository(AnalysisEntity); + data.realm_id = data.project.realm_id; + const repository = dataSource.getRepository(AnalysisEntity); const identity = useRequestIdentityOrFail(req); const entity = repository.create({ realm_id: identity.realmId, - ...result.data, + ...data, }); switch (identity.type) { case 'user': { @@ -60,9 +69,9 @@ export async function createAnalysisRouteHandler(req: Request, res: Response) : await repository.save(entity); - result.relation.project.analyses++; + entity.project.analyses++; const proposalRepository = dataSource.getRepository(ProjectEntity); - await proposalRepository.save(result.relation.project); + await proposalRepository.save(entity.project); await runAnalysisSpinUpCommand(entity); diff --git a/packages/server-core/src/http/controllers/analysis/handlers/update.ts b/packages/server-core/src/http/controllers/analysis/handlers/update.ts index 364a88221..6cb4e2462 100644 --- a/packages/server-core/src/http/controllers/analysis/handlers/update.ts +++ b/packages/server-core/src/http/controllers/analysis/handlers/update.ts @@ -10,10 +10,11 @@ import { BadRequestError, ForbiddenError, NotFoundError } from '@ebec/http'; import { PermissionName } from '@privateaim/kit'; import type { Request, Response } from 'routup'; import { sendAccepted, useRequestParam } from 'routup'; -import { useDataSource } from 'typeorm-extension'; -import { useRequestIdentityRealm, useRequestPermissionChecker } from '@privateaim/server-http-kit'; +import { useDataSource, validateEntityJoinColumns } from 'typeorm-extension'; +import { HTTPHandlerOperation, useRequestIdentityRealm, useRequestPermissionChecker } from '@privateaim/server-http-kit'; +import { RoutupContainerAdapter } from '@validup/adapter-routup'; import { AnalysisEntity } from '../../../../domains'; -import { runAnalysisValidation } from '../utils'; +import { AnalysisValidator } from '../utils'; export async function updateAnalysisRouteHandler(req: Request, res: Response) : Promise { const id = useRequestParam(req, 'id'); @@ -21,15 +22,22 @@ export async function updateAnalysisRouteHandler(req: Request, res: Response) : const permissionChecker = useRequestPermissionChecker(req); await permissionChecker.preCheck({ name: PermissionName.ANALYSIS_UPDATE }); - const result = await runAnalysisValidation(req, 'update'); - if (!result.data) { - return sendAccepted(res); - } + const validator = new AnalysisValidator(); + const validatorAdapter = new RoutupContainerAdapter(validator); + const data = await validatorAdapter.run(req, { + group: HTTPHandlerOperation.UPDATE, + }); const dataSource = await useDataSource(); + + // todo: this should only validate non null/undefined values + await validateEntityJoinColumns(data, { + dataSource, + entityTarget: AnalysisEntity, + }); + const repository = dataSource.getRepository(AnalysisEntity); let entity = await repository.findOneBy({ id }); - if (!entity) { throw new NotFoundError(); } @@ -47,13 +55,13 @@ export async function updateAnalysisRouteHandler(req: Request, res: Response) : if ( entity.registry_id && - result.data.registry_id && - entity.registry_id !== result.data.registry_id + data.registry_id && + entity.registry_id !== data.registry_id ) { throw new BadRequestError('The registry can not be changed after it is specified.'); } - entity = repository.merge(entity, result.data); + entity = repository.merge(entity, data); await repository.save(entity); diff --git a/packages/server-core/src/http/controllers/analysis/utils/index.ts b/packages/server-core/src/http/controllers/analysis/utils/index.ts index 0a6d8bd4c..0a1027e59 100644 --- a/packages/server-core/src/http/controllers/analysis/utils/index.ts +++ b/packages/server-core/src/http/controllers/analysis/utils/index.ts @@ -5,4 +5,5 @@ * view the LICENSE file that was distributed with this source code. */ -export * from './validation'; +export * from './validator-command'; +export * from './validator'; diff --git a/packages/server-core/src/http/controllers/analysis/utils/validation.ts b/packages/server-core/src/http/controllers/analysis/utils/validation.ts deleted file mode 100644 index 58e028e52..000000000 --- a/packages/server-core/src/http/controllers/analysis/utils/validation.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) 2022-2024. - * Author Peter Placzek (tada5hi) - * For the full copyright and license information, - * view the LICENSE file that was distributed with this source code. - */ - -import { check } from 'express-validator'; -import { BadRequestError } from '@ebec/http'; -import { isRealmResourceWritable } from '@authup/core-kit'; -import type { Request } from 'routup'; -import type { HTTPValidationResult } from '@privateaim/server-http-kit'; -import { - createHTTPValidationResult, - extendHTTPValidationResultWithRelation, - useRequestIdentityRealm, -} from '@privateaim/server-http-kit'; -import { - MasterImageEntity, ProjectEntity, RegistryEntity, -} from '../../../../domains'; -import type { AnalysisEntity } from '../../../../domains'; - -export async function runAnalysisValidation( - req: Request, - operation: 'create' | 'update', -) : Promise> { - if (operation === 'create') { - await check('project_id') - .exists() - .notEmpty() - .isUUID() - .run(req); - } - - await check('name') - .notEmpty() - .isLength({ min: 1, max: 128 }) - .optional({ nullable: true }) - .run(req); - - await check('description') - .isString() - .isLength({ min: 5, max: 4096 }) - .optional({ values: 'null' }) - .run(req); - - await check('master_image_id') - .exists() - .notEmpty() - .isUUID() - .optional({ nullable: true }) - .run(req); - - await check('registry_id') - .exists() - .notEmpty() - .isUUID() - .optional({ nullable: true }) - .run(req); - - // ---------------------------------------------- - - const result = createHTTPValidationResult(req); - - // ---------------------------------------------- - - await extendHTTPValidationResultWithRelation(result, MasterImageEntity, { - id: 'master_image_id', - entity: 'master_image', - }); - - await extendHTTPValidationResultWithRelation(result, ProjectEntity, { - id: 'project_id', - entity: 'project', - }); - await extendHTTPValidationResultWithRelation(result, RegistryEntity, { - id: 'registry_id', - entity: 'registry', - }); - - if (result.relation.project) { - if (!isRealmResourceWritable(useRequestIdentityRealm(req), result.relation.project.realm_id)) { - throw new BadRequestError('The referenced project realm is not permitted.'); - } - - result.data.realm_id = result.relation.project.realm_id; - } - - return result; -} diff --git a/packages/server-core/src/http/controllers/analysis/utils/validator-command.ts b/packages/server-core/src/http/controllers/analysis/utils/validator-command.ts new file mode 100644 index 000000000..1795ef871 --- /dev/null +++ b/packages/server-core/src/http/controllers/analysis/utils/validator-command.ts @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import { AnalysisAPICommand } from '@privateaim/core-kit'; +import { createValidator } from '@validup/adapter-validator'; +import { Container } from 'validup'; + +export class AnalysisCommandValidator extends Container<{ command: AnalysisAPICommand }> { + protected initialize() { + super.initialize(); + + this.mount( + 'command', + createValidator((chain) => chain + .exists() + .custom((command) => Object.values(AnalysisAPICommand).includes(command))), + ); + } +} diff --git a/packages/server-core/src/http/controllers/analysis/utils/validator.ts b/packages/server-core/src/http/controllers/analysis/utils/validator.ts new file mode 100644 index 000000000..9cdd8478e --- /dev/null +++ b/packages/server-core/src/http/controllers/analysis/utils/validator.ts @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2022-2024. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import { Container } from 'validup'; +import { createValidator } from '@validup/adapter-validator'; +import type { Analysis } from '@privateaim/core-kit'; +import { HTTPHandlerOperation } from '@privateaim/server-http-kit'; + +export class AnalysisValidator extends Container { + protected initialize() { + super.initialize(); + + this.mount( + 'project_id', + { group: HTTPHandlerOperation.CREATE }, + createValidator((chain) => chain + .exists() + .notEmpty() + .isUUID()), + ); + + this.mount( + 'name', + { optional: true }, + createValidator((chain) => chain + .isString() + .isLength({ min: 3, max: 128 }) + .optional({ values: 'null' })), + ); + + this.mount( + 'description', + { optional: true }, + createValidator((chain) => chain + .isString() + .isLength({ min: 5, max: 4096 }) + .optional({ values: 'null' })), + ); + + this.mount( + 'master_image_id', + { optional: true }, + createValidator((chain) => chain + .isUUID() + .optional({ values: 'null' })), + ); + + this.mount( + 'registry_id', + { optional: true }, + createValidator((chain) => chain + .isUUID() + .optional({ values: 'null' })), + ); + } +} diff --git a/packages/server-core/src/http/controllers/node/handlers/create.ts b/packages/server-core/src/http/controllers/node/handlers/create.ts index 8be90f541..cfba83713 100644 --- a/packages/server-core/src/http/controllers/node/handlers/create.ts +++ b/packages/server-core/src/http/controllers/node/handlers/create.ts @@ -11,29 +11,51 @@ import { import { PermissionName, createNanoID, isHex } from '@privateaim/kit'; import type { Request, Response } from 'routup'; import { sendCreated } from 'routup'; -import { useDataSource } from 'typeorm-extension'; -import { useRequestPermissionChecker } from '@privateaim/server-http-kit'; +import { useDataSource, validateEntityJoinColumns } from 'typeorm-extension'; +import { HTTPHandlerOperation, useRequestIdentityRealm, useRequestPermissionChecker } from '@privateaim/server-http-kit'; import { isQueueRouterUsable, useQueueRouter } from '@privateaim/server-kit'; +import { isRealmResourceWritable } from '@authup/core-kit'; +import { ForbiddenError } from '@ebec/http'; +import { RoutupContainerAdapter } from '@validup/adapter-routup'; import { RegistryCommand, buildRegistryTaskQueueRouterPayload } from '../../../../components'; -import { createNodeRobot, runNodeValidation } from '../utils'; -import { NodeEntity, RegistryEntity, RegistryProjectEntity } from '../../../../domains'; +import { NodeValidator, createNodeRobot } from '../utils'; +import { + NodeEntity, RegistryEntity, RegistryProjectEntity, +} from '../../../../domains'; export async function createNodeRouteHandler(req: Request, res: Response) : Promise { const permissionChecker = useRequestPermissionChecker(req); await permissionChecker.preCheck({ name: PermissionName.NODE_CREATE }); - const result = await runNodeValidation(req, 'create'); + const validator = new NodeValidator(); + const validatorAdapter = new RoutupContainerAdapter(validator); + const data = await validatorAdapter.run(req, { + group: HTTPHandlerOperation.CREATE, + }); const dataSource = await useDataSource(); - const repository = dataSource.getRepository(NodeEntity); + await validateEntityJoinColumns(data, { + dataSource, + entityTarget: NodeEntity, + }); + + const realm = useRequestIdentityRealm(req); + if (data.realm_id) { + if (!isRealmResourceWritable(realm, data.realm_id)) { + throw new ForbiddenError('You are not permitted to create this node.'); + } + } else { + data.realm_id = realm.id; + } - const entity = repository.create(result.data); + const repository = dataSource.getRepository(NodeEntity); + const entity = repository.create(data); // ----------------------------------------------------- if ( entity.public_key && - !isHex(result.data.public_key) + !isHex(data.public_key) ) { entity.public_key = Buffer .from(entity.public_key, 'utf8') diff --git a/packages/server-core/src/http/controllers/node/handlers/update.ts b/packages/server-core/src/http/controllers/node/handlers/update.ts index 4b030806b..d51bf6912 100644 --- a/packages/server-core/src/http/controllers/node/handlers/update.ts +++ b/packages/server-core/src/http/controllers/node/handlers/update.ts @@ -13,11 +13,12 @@ import { ForbiddenError, NotFoundError } from '@ebec/http'; import { isRealmResourceWritable } from '@authup/core-kit'; import type { Request, Response } from 'routup'; import { sendAccepted, useRequestParam } from 'routup'; -import { useDataSource } from 'typeorm-extension'; -import { useRequestIdentityRealm, useRequestPermissionChecker } from '@privateaim/server-http-kit'; +import { useDataSource, validateEntityJoinColumns } from 'typeorm-extension'; +import { HTTPHandlerOperation, useRequestIdentityRealm, useRequestPermissionChecker } from '@privateaim/server-http-kit'; import { isQueueRouterUsable, useQueueRouter } from '@privateaim/server-kit'; +import { RoutupContainerAdapter } from '@validup/adapter-routup'; import { RegistryCommand, buildRegistryTaskQueueRouterPayload } from '../../../../components'; -import { createNodeRobot, runNodeValidation } from '../utils'; +import { NodeValidator, createNodeRobot } from '../utils'; import { NodeEntity, RegistryProjectEntity } from '../../../../domains'; export async function updateNodeRouteHandler(req: Request, res: Response) : Promise { @@ -26,30 +27,35 @@ export async function updateNodeRouteHandler(req: Request, res: Response) : Prom const permissionChecker = useRequestPermissionChecker(req); await permissionChecker.preCheck({ name: PermissionName.NODE_UPDATE }); - const result = await runNodeValidation(req, 'update'); - if (!result.data) { - return sendAccepted(res); - } + const validator = new NodeValidator(); + const validatorAdapter = new RoutupContainerAdapter(validator); + const data = await validatorAdapter.run(req, { + group: HTTPHandlerOperation.UPDATE, + }); + + const dataSource = await useDataSource(); + await validateEntityJoinColumns(data, { + dataSource, + entityTarget: NodeEntity, + }); if ( - result.data.public_key && - !isHex(result.data.public_key) + data.public_key && + !isHex(data.public_key) ) { - result.data.public_key = Buffer - .from(result.data.public_key, 'utf8') + data.public_key = Buffer + .from(data.public_key, 'utf8') .toString('hex'); } - const dataSource = await useDataSource(); const repository = dataSource.getRepository(NodeEntity); - const query = repository.createQueryBuilder('station') + const query = repository.createQueryBuilder('node') .addSelect([ - 'station.external_name', + 'node.external_name', ]) - .where('station.id = :id', { id }); + .where('node.id = :id', { id }); let entity = await query.getOne(); - if (!entity) { throw new NotFoundError(); } @@ -58,7 +64,7 @@ export async function updateNodeRouteHandler(req: Request, res: Response) : Prom throw new ForbiddenError('You are not permitted to delete this station.'); } - entity = repository.merge(entity, result.data); + entity = repository.merge(entity, data); if (entity.registry_id) { const registryProjectExternalName = entity.external_name || createNanoID(); diff --git a/packages/server-core/src/http/controllers/node/utils/validation.ts b/packages/server-core/src/http/controllers/node/utils/validation.ts index e1a598484..a430e438c 100644 --- a/packages/server-core/src/http/controllers/node/utils/validation.ts +++ b/packages/server-core/src/http/controllers/node/utils/validation.ts @@ -6,114 +6,90 @@ */ import { NodeType } from '@privateaim/core-kit'; -import { check } from 'express-validator'; -import { isRealmResourceWritable } from '@authup/core-kit'; -import { ForbiddenError } from '@ebec/http'; -import type { Request } from 'routup'; -import type { HTTPValidationResult } from '@privateaim/server-http-kit'; -import { - createHTTPValidationResult, - extendHTTPValidationResultWithRelation, - useRequestIdentityRealm, -} from '@privateaim/server-http-kit'; -import { RegistryEntity } from '../../../../domains'; +import { Container } from 'validup'; +import { createValidator } from '@validup/adapter-validator'; +import { HTTPHandlerOperation } from '@privateaim/server-http-kit'; import type { NodeEntity } from '../../../../domains'; -export async function runNodeValidation( - req: Request, - operation: 'create' | 'update', -) : Promise> { - const nameChain = check('name') - .isLength({ min: 3, max: 128 }) - .exists() - .notEmpty(); - - if (operation === 'update') { - nameChain.optional(); +export class NodeValidator extends Container { + protected initialize() { + super.initialize(); + + this.mount( + 'name', + { group: HTTPHandlerOperation.CREATE }, + createValidator((chain) => chain + .isLength({ min: 3, max: 128 }) + .exists() + .notEmpty()), + ); + + this.mount( + 'name', + { group: HTTPHandlerOperation.UPDATE, optional: true }, + createValidator((chain) => chain + .isLength({ min: 3, max: 128 }) + .optional({ values: 'null' })), + ); + + this.mount( + 'type', + { optional: true }, + createValidator((chain) => chain + .isIn(Object.values(NodeType)) + .optional({ values: 'null' })), + ); + + this.mount( + 'hidden', + { optional: true }, + createValidator((chain) => chain + .isBoolean() + .optional({ values: 'null' })), + ); + + this.mount( + 'public_key', + { optional: true }, + createValidator((chain) => chain + .isLength({ min: 5, max: 4096 }) + .exists() + .optional({ values: 'null' })), + ); + + this.mount( + 'external_name', + { optional: true }, + createValidator((chain) => chain + .isLength({ min: 1, max: 64 }) + .exists() + .matches(/^[a-z0-9-_]*$/) + .optional({ nullable: true })), + ); + + this.mount( + 'registry_id', + { optional: true }, + createValidator((chain) => chain + .isUUID() + .optional({ nullable: true })), + ); + + this.mount( + 'robot_id', + { optional: true }, + createValidator((chain) => chain + .isUUID() + .optional({ nullable: true })), + ); + + this.mount( + 'realm_id', + { group: HTTPHandlerOperation.CREATE, optional: true }, + createValidator((chain) => chain + .exists() + .isUUID() + .optional({ nullable: true })), + ); } - - await nameChain.run(req); - - // ------------------------------------------------------------- - - await check('type') - .isIn(Object.values(NodeType)) - .optional() - .run(req); - - // ------------------------------------------------------------- - - await check('hidden') - .isBoolean() - .optional() - .run(req); - - // ------------------------------------------------------------- - - await check('public_key') - .isLength({ min: 5, max: 4096 }) - .exists() - .optional({ nullable: true }) - .run(req); - - // ------------------------------------------------------------- - - await check('external_name') - .isLength({ min: 1, max: 64 }) - .exists() - .matches(/^[a-z0-9-_]*$/) - .optional({ nullable: true }) - .run(req); - - // ------------------------------------------------------------- - - await check('registry_id') - .exists() - .isUUID() - .optional({ nullable: true }) - .run(req); - - // ------------------------------------------------------------- - - await check('robot_id') - .exists() - .isUUID() - .optional({ nullable: true }) - .run(req); - - // ------------------------------------------------------------- - - if (operation === 'create') { - await check('realm_id') - .exists() - .isUUID() - .optional({ nullable: true }) - .run(req); - } - - // ---------------------------------------------- - - const result = createHTTPValidationResult(req); - - // ---------------------------------------------- - - await extendHTTPValidationResultWithRelation(result, RegistryEntity, { - id: 'registry_id', - entity: 'registry', - }); - - // ---------------------------------------------- - - if (operation === 'create') { - const realm = useRequestIdentityRealm(req); - if (result.data.realm_id) { - if (!isRealmResourceWritable(realm, result.data.realm_id)) { - throw new ForbiddenError('You are not permitted to create this node.'); - } - } else { - result.data.realm_id = realm.id; - } - } - - return result; } diff --git a/packages/server-core/src/http/controllers/project-node/handlers/create.ts b/packages/server-core/src/http/controllers/project-node/handlers/create.ts index 62af32da7..95903f3bc 100644 --- a/packages/server-core/src/http/controllers/project-node/handlers/create.ts +++ b/packages/server-core/src/http/controllers/project-node/handlers/create.ts @@ -9,10 +9,13 @@ import { NodeType, ProjectNodeApprovalStatus } from '@privateaim/core-kit'; import { PermissionName } from '@privateaim/kit'; import type { Request, Response } from 'routup'; import { sendCreated } from 'routup'; -import { useDataSource } from 'typeorm-extension'; -import { useRequestPermissionChecker } from '@privateaim/server-http-kit'; +import { useDataSource, validateEntityJoinColumns } from 'typeorm-extension'; +import { HTTPHandlerOperation, useRequestIdentityRealm, useRequestPermissionChecker } from '@privateaim/server-http-kit'; +import { isRealmResourceWritable } from '@authup/core-kit'; +import { NotFoundError } from '@ebec/http'; +import { RoutupContainerAdapter } from '@validup/adapter-routup'; import { ProjectEntity, ProjectNodeEntity } from '../../../../domains'; -import { runProjectNodeValidation } from '../utils'; +import { ProjectNodeValidator } from '../utils'; import { useEnv } from '../../../../config'; export async function createProjectNodeRouteHandler(req: Request, res: Response) : Promise { @@ -24,24 +27,40 @@ export async function createProjectNodeRouteHandler(req: Request, res: Response) ], }); - const result = await runProjectNodeValidation(req, 'create'); + const validator = new ProjectNodeValidator(); + const validatorAdapter = new RoutupContainerAdapter(validator); + const data = await validatorAdapter.run(req, { + group: HTTPHandlerOperation.CREATE, + }); const dataSource = await useDataSource(); + await validateEntityJoinColumns(data, { + dataSource, + entityTarget: ProjectNodeEntity, + }); + + data.project_realm_id = data.project.realm_id; + data.node_realm_id = data.node.realm_id; + + if (!isRealmResourceWritable(useRequestIdentityRealm(req), data.project.realm_id)) { + throw new NotFoundError('The referenced project realm is not permitted.'); + } + const repository = dataSource.getRepository(ProjectNodeEntity); - let entity = repository.create(result.data); + let entity = repository.create(data); if ( useEnv('skipProjectApproval') || - (result.relation.node && result.relation.node.type === NodeType.AGGREGATOR) + (data.node.type === NodeType.AGGREGATOR) ) { entity.approval_status = ProjectNodeApprovalStatus.APPROVED; } entity = await repository.save(entity); - result.relation.project.nodes += 1; + data.project.nodes += 1; const projectRepository = dataSource.getRepository(ProjectEntity); - await projectRepository.save(result.relation.project); + await projectRepository.save(data.project); return sendCreated(res, entity); } diff --git a/packages/server-core/src/http/controllers/project-node/handlers/update.ts b/packages/server-core/src/http/controllers/project-node/handlers/update.ts index 28c56ed92..34fbbb0c3 100644 --- a/packages/server-core/src/http/controllers/project-node/handlers/update.ts +++ b/packages/server-core/src/http/controllers/project-node/handlers/update.ts @@ -11,17 +11,23 @@ import { PermissionName } from '@privateaim/kit'; import type { Request, Response } from 'routup'; import { sendAccepted, useRequestParam } from 'routup'; import { useDataSource } from 'typeorm-extension'; -import { useRequestIdentityRealm, useRequestPermissionChecker } from '@privateaim/server-http-kit'; +import { HTTPHandlerOperation, useRequestIdentityRealm, useRequestPermissionChecker } from '@privateaim/server-http-kit'; +import { RoutupContainerAdapter } from '@validup/adapter-routup'; import { ProjectNodeEntity } from '../../../../domains'; -import { runProjectNodeValidation } from '../utils'; +import { ProjectNodeValidator } from '../utils'; export async function updateProjectNodeRouteHandler(req: Request, res: Response) : Promise { + const validator = new ProjectNodeValidator(); + const validatorAdapter = new RoutupContainerAdapter(validator); + const data = await validatorAdapter.run(req, { + group: HTTPHandlerOperation.CREATE, + }); + const id = useRequestParam(req, 'id'); const dataSource = await useDataSource(); const repository = dataSource.getRepository(ProjectNodeEntity); let entity = await repository.findOneBy({ id }); - if (!entity) { throw new NotFoundError(); } @@ -35,9 +41,7 @@ export async function updateProjectNodeRouteHandler(req: Request, res: Response) await permissionChecker.preCheck({ name: PermissionName.PROJECT_APPROVE }); - const result = await runProjectNodeValidation(req, 'update'); - - entity = repository.merge(entity, result.data); + entity = repository.merge(entity, data); entity = await repository.save(entity); diff --git a/packages/server-core/src/http/controllers/project-node/utils/validation.ts b/packages/server-core/src/http/controllers/project-node/utils/validation.ts index bf7d24d57..29fa57143 100644 --- a/packages/server-core/src/http/controllers/project-node/utils/validation.ts +++ b/packages/server-core/src/http/controllers/project-node/utils/validation.ts @@ -5,75 +5,49 @@ * view the LICENSE file that was distributed with this source code. */ -import { check } from 'express-validator'; import { ProjectNodeApprovalStatus } from '@privateaim/core-kit'; -import { NotFoundError } from '@ebec/http'; -import { isRealmResourceWritable } from '@authup/core-kit'; -import type { Request } from 'routup'; -import type { HTTPValidationResult } from '@privateaim/server-http-kit'; -import { - createHTTPValidationResult, - extendHTTPValidationResultWithRelation, - useRequestIdentityRealm, -} from '@privateaim/server-http-kit'; +import { Container } from 'validup'; +import { createValidator } from '@validup/adapter-validator'; +import { HTTPHandlerOperation } from '@privateaim/server-http-kit'; import type { ProjectNodeEntity } from '../../../../domains'; -import { NodeEntity, ProjectEntity } from '../../../../domains'; -export async function runProjectNodeValidation( - req: Request, - operation: 'create' | 'update', -) : Promise> { - if (operation === 'create') { - await check('project_id') - .exists() - .isUUID() - .run(req); - - await check('node_id') - .exists() - .isUUID() - .run(req); - } - - if (operation === 'update') { - await check('approval_status') - .optional() - .isIn(Object.values(ProjectNodeApprovalStatus)) - .run(req); - - await check('comment') - .optional({ nullable: true }) - .isString() - .isLength({ min: 3, max: 4096 }) - .run(req); - } - - // ---------------------------------------------- - - const result = createHTTPValidationResult(req); - - // ---------------------------------------------- - - await extendHTTPValidationResultWithRelation(result, ProjectEntity, { - id: 'project_id', - entity: 'project', - }); - - if (result.relation.project) { - result.data.project_realm_id = result.relation.project.realm_id; - - if (!isRealmResourceWritable(useRequestIdentityRealm(req), result.relation.project.realm_id)) { - throw new NotFoundError('The referenced project realm is not permitted.'); - } +export class ProjectNodeValidator extends Container { + protected initialize() { + super.initialize(); + + this.mount( + 'project_id', + { group: HTTPHandlerOperation.CREATE }, + createValidator((chain) => chain + .exists() + .notEmpty() + .isUUID()), + ); + + this.mount( + 'node_id', + { group: HTTPHandlerOperation.CREATE }, + createValidator((chain) => chain + .exists() + .notEmpty() + .isUUID()), + ); + + this.mount( + 'approval_status', + { group: HTTPHandlerOperation.UPDATE }, + createValidator((chain) => chain + .optional({ values: 'null' }) + .isIn(Object.values(ProjectNodeApprovalStatus))), + ); + + this.mount( + 'comment', + { group: HTTPHandlerOperation.UPDATE, optional: true }, + createValidator((chain) => chain + .optional({ nullable: true }) + .isString() + .isLength({ min: 3, max: 4096 })), + ); } - - await extendHTTPValidationResultWithRelation(result, NodeEntity, { - id: 'node_id', - entity: 'node', - }); - if (result.relation.node) { - result.data.node_realm_id = result.relation.node.realm_id; - } - - return result; } diff --git a/packages/server-core/src/http/controllers/project/handlers/create.ts b/packages/server-core/src/http/controllers/project/handlers/create.ts index 131d2b59e..d8f128ecd 100644 --- a/packages/server-core/src/http/controllers/project/handlers/create.ts +++ b/packages/server-core/src/http/controllers/project/handlers/create.ts @@ -5,30 +5,56 @@ * view the LICENSE file that was distributed with this source code. */ -import { isEntityUnique, useDataSource } from 'typeorm-extension'; -import { BadRequestError } from '@ebec/http'; +import { isEntityUnique, useDataSource, validateEntityJoinColumns } from 'typeorm-extension'; +import { BadRequestError, ForbiddenError } from '@ebec/http'; import { PermissionName } from '@privateaim/kit'; import type { Request, Response } from 'routup'; import { sendCreated } from 'routup'; -import { useRequestIdentityOrFail, useRequestPermissionChecker } from '@privateaim/server-http-kit'; +import { + HTTPHandlerOperation, + useRequestIdentityOrFail, + useRequestIdentityRealm, + useRequestPermissionChecker, +} from '@privateaim/server-http-kit'; +import { isRealmResourceWritable } from '@authup/core-kit'; +import { RoutupContainerAdapter } from '@validup/adapter-routup'; import { DatabaseConflictError } from '../../../../database'; import { ProjectEntity } from '../../../../domains'; -import { runProjectValidation } from '../utils/validation'; +import { ProjectValidator } from '../utils/validator'; export async function createProjectRouteHandler(req: Request, res: Response) : Promise { const permissionChecker = useRequestPermissionChecker(req); await permissionChecker.preCheck({ name: PermissionName.PROJECT_CREATE }); - const result = await runProjectValidation(req, 'create'); + const validator = new ProjectValidator(); + const validatorAdapter = new RoutupContainerAdapter(validator); + const data = await validatorAdapter.run(req, { + group: HTTPHandlerOperation.CREATE, + }); + + const dataSource = await useDataSource(); + await validateEntityJoinColumns(data, { + dataSource, + entityTarget: ProjectEntity, + }); + + const realm = useRequestIdentityRealm(req); + if (data.realm_id) { + if (!isRealmResourceWritable(realm, data.realm_id)) { + throw new ForbiddenError('You are not permitted to create this project.'); + } + } else { + data.realm_id = realm.id; + } const identity = useRequestIdentityOrFail(req); switch (identity.type) { case 'user': { - result.data.user_id = identity.id; + data.user_id = identity.id; break; } case 'robot': { - result.data.robot_id = identity.id; + data.robot_id = identity.id; break; } default: { @@ -36,9 +62,8 @@ export async function createProjectRouteHandler(req: Request, res: Response) : P } } - const dataSource = await useDataSource(); const repository = dataSource.getRepository(ProjectEntity); - const entity = repository.create(result.data); + const entity = repository.create(data); const isUnique = await isEntityUnique({ entity, diff --git a/packages/server-core/src/http/controllers/project/handlers/update.ts b/packages/server-core/src/http/controllers/project/handlers/update.ts index 39c93f96a..0496d72a9 100644 --- a/packages/server-core/src/http/controllers/project/handlers/update.ts +++ b/packages/server-core/src/http/controllers/project/handlers/update.ts @@ -11,10 +11,11 @@ import { PermissionName } from '@privateaim/kit'; import type { Request, Response } from 'routup'; import { sendAccepted, useRequestParam } from 'routup'; import { isEntityUnique, useDataSource } from 'typeorm-extension'; -import { useRequestIdentityRealm, useRequestPermissionChecker } from '@privateaim/server-http-kit'; +import { HTTPHandlerOperation, useRequestIdentityRealm, useRequestPermissionChecker } from '@privateaim/server-http-kit'; +import { RoutupContainerAdapter } from '@validup/adapter-routup'; import { DatabaseConflictError } from '../../../../database'; import { ProjectEntity } from '../../../../domains'; -import { runProjectValidation } from '../utils/validation'; +import { ProjectValidator } from '../utils/validator'; export async function updateProjectRouteHandler(req: Request, res: Response) : Promise { const id = useRequestParam(req, 'id'); @@ -22,15 +23,15 @@ export async function updateProjectRouteHandler(req: Request, res: Response) : P const permissionChecker = useRequestPermissionChecker(req); await permissionChecker.preCheck({ name: PermissionName.PROJECT_UPDATE }); - const result = await runProjectValidation(req, 'update'); - if (!result.data) { - return sendAccepted(res); - } + const validator = new ProjectValidator(); + const validatorAdapter = new RoutupContainerAdapter(validator); + const data = await validatorAdapter.run(req, { + group: HTTPHandlerOperation.UPDATE, + }); const dataSource = await useDataSource(); const repository = dataSource.getRepository(ProjectEntity); let entity = await repository.findOneBy({ id }); - if (!entity) { throw new NotFoundError(); } @@ -39,7 +40,7 @@ export async function updateProjectRouteHandler(req: Request, res: Response) : P throw new ForbiddenError(); } - entity = repository.merge(entity, result.data); + entity = repository.merge(entity, data); const isUnique = await isEntityUnique({ entity, diff --git a/packages/server-core/src/http/controllers/project/utils/validation.ts b/packages/server-core/src/http/controllers/project/utils/validation.ts deleted file mode 100644 index 8b4323996..000000000 --- a/packages/server-core/src/http/controllers/project/utils/validation.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (c) 2022-2024. - * Author Peter Placzek (tada5hi) - * For the full copyright and license information, - * view the LICENSE file that was distributed with this source code. - */ - -import { isRealmResourceWritable } from '@authup/core-kit'; -import { ForbiddenError } from '@ebec/http'; -import { check } from 'express-validator'; -import type { Request } from 'routup'; -import type { HTTPValidationResult } from '@privateaim/server-http-kit'; -import { - createHTTPValidationResult, - extendHTTPValidationResultWithRelation, - useRequestIdentityRealm, -} from '@privateaim/server-http-kit'; -import { MasterImageEntity } from '../../../../domains'; -import type { ProjectEntity } from '../../../../domains'; - -export async function runProjectValidation( - req: Request, - operation: 'create' | 'update', -) : Promise> { - const titleChain = check('name') - .exists() - .isLength({ min: 5, max: 100 }); - - if (operation === 'update') { - titleChain.optional(); - } - - await titleChain.run(req); - - // ---------------------------------------------- - - await check('description') - .isString() - .isLength({ min: 5, max: 4096 }) - .optional({ values: 'null' }) - .run(req); - - // ---------------------------------------------- - - await check('master_image_id') - .isUUID() - .optional({ nullable: true }) - .run(req); - - // ---------------------------------------------- - - const result = createHTTPValidationResult(req); - - // ---------------------------------------------- - - await extendHTTPValidationResultWithRelation(result, MasterImageEntity, { - id: 'master_image_id', - entity: 'master_image', - }); - - // ---------------------------------------------- - - if (operation === 'create') { - const realm = useRequestIdentityRealm(req); - if (result.data.realm_id) { - if (!isRealmResourceWritable(realm, result.data.realm_id)) { - throw new ForbiddenError('You are not permitted to create this project.'); - } - } else { - result.data.realm_id = realm.id; - } - } - - return result; -} diff --git a/packages/server-core/src/http/controllers/project/utils/validator.ts b/packages/server-core/src/http/controllers/project/utils/validator.ts new file mode 100644 index 000000000..e27327c51 --- /dev/null +++ b/packages/server-core/src/http/controllers/project/utils/validator.ts @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2022-2024. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import { Container } from 'validup'; +import { createValidator } from '@validup/adapter-validator'; +import { HTTPHandlerOperation } from '@privateaim/server-http-kit'; +import type { ProjectEntity } from '../../../../domains'; + +export class ProjectValidator extends Container { + protected initialize() { + super.initialize(); + + this.mount( + 'name', + { group: HTTPHandlerOperation.CREATE }, + createValidator((chain) => chain + .exists() + .isLength({ min: 5, max: 100 })), + ); + + this.mount( + 'name', + { group: HTTPHandlerOperation.UPDATE, optional: true }, + createValidator((chain) => chain + .exists() + .isLength({ min: 5, max: 100 })), + ); + + this.mount( + 'description', + { optional: true }, + createValidator((chain) => chain + .isString() + .isLength({ min: 5, max: 4096 }) + .optional({ values: 'null' })), + ); + + this.mount( + 'master_image_id', + { optional: true }, + createValidator((chain) => chain + .isUUID() + .optional({ nullable: true })), + ); + } +} diff --git a/packages/server-core/src/http/controllers/registry-project/handlers/create.ts b/packages/server-core/src/http/controllers/registry-project/handlers/create.ts index da1f11a2f..459d8d083 100644 --- a/packages/server-core/src/http/controllers/registry-project/handlers/create.ts +++ b/packages/server-core/src/http/controllers/registry-project/handlers/create.ts @@ -8,22 +8,32 @@ import { PermissionName } from '@privateaim/kit'; import type { Request, Response } from 'routup'; import { sendCreated } from 'routup'; -import { useDataSource } from 'typeorm-extension'; -import { useRequestPermissionChecker } from '@privateaim/server-http-kit'; +import { useDataSource, validateEntityJoinColumns } from 'typeorm-extension'; +import { HTTPHandlerOperation, useRequestPermissionChecker } from '@privateaim/server-http-kit'; import { isQueueRouterUsable, useQueueRouter } from '@privateaim/server-kit'; +import { RoutupContainerAdapter } from '@validup/adapter-routup'; import { RegistryCommand, buildRegistryTaskQueueRouterPayload } from '../../../../components'; -import { runRegistryProjectValidation } from '../utils'; +import { RegistryProjectValidator } from '../utils'; import { RegistryProjectEntity } from '../../../../domains'; export async function createRegistryProjectRouteHandler(req: Request, res: Response) : Promise { const permissionChecker = useRequestPermissionChecker(req); await permissionChecker.preCheck({ name: PermissionName.REGISTRY_PROJECT_MANAGE }); - const result = await runRegistryProjectValidation(req, 'create'); + const validator = new RegistryProjectValidator(); + const validatorAdapter = new RoutupContainerAdapter(validator); + const data = await validatorAdapter.run(req, { + group: HTTPHandlerOperation.CREATE, + }); const dataSource = await useDataSource(); + await validateEntityJoinColumns(data, { + dataSource, + entityTarget: RegistryProjectEntity, + }); + const repository = dataSource.getRepository(RegistryProjectEntity); - const entity = repository.create(result.data); + const entity = repository.create(data); await repository.save(entity); diff --git a/packages/server-core/src/http/controllers/registry-project/handlers/index.ts b/packages/server-core/src/http/controllers/registry-project/handlers/index.ts index f93242a16..f4eb46b0b 100644 --- a/packages/server-core/src/http/controllers/registry-project/handlers/index.ts +++ b/packages/server-core/src/http/controllers/registry-project/handlers/index.ts @@ -9,4 +9,4 @@ export * from './create'; export * from './delete'; export * from './read'; export * from './update'; -export * from '../utils/validation'; +export * from '../utils/validator'; diff --git a/packages/server-core/src/http/controllers/registry-project/handlers/update.ts b/packages/server-core/src/http/controllers/registry-project/handlers/update.ts index cfe1cfab7..619dbbb4b 100644 --- a/packages/server-core/src/http/controllers/registry-project/handlers/update.ts +++ b/packages/server-core/src/http/controllers/registry-project/handlers/update.ts @@ -11,10 +11,11 @@ import type { Request, Response } from 'routup'; import { sendAccepted, useRequestParam } from 'routup'; import { useDataSource } from 'typeorm-extension'; import { isRealmResourceWritable } from '@authup/core-kit'; -import { useRequestIdentityRealm, useRequestPermissionChecker } from '@privateaim/server-http-kit'; +import { HTTPHandlerOperation, useRequestIdentityRealm, useRequestPermissionChecker } from '@privateaim/server-http-kit'; import { isQueueRouterUsable, useQueueRouter } from '@privateaim/server-kit'; +import { RoutupContainerAdapter } from '@validup/adapter-routup'; import { RegistryCommand, buildRegistryTaskQueueRouterPayload } from '../../../../components'; -import { runRegistryProjectValidation } from '../utils'; +import { RegistryProjectValidator } from '../utils'; import { RegistryProjectEntity } from '../../../../domains'; export async function updateRegistryProjectRouteHandler(req: Request, res: Response) : Promise { @@ -23,10 +24,11 @@ export async function updateRegistryProjectRouteHandler(req: Request, res: Respo const permissionChecker = useRequestPermissionChecker(req); await permissionChecker.preCheck({ name: PermissionName.REGISTRY_PROJECT_MANAGE }); - const result = await runRegistryProjectValidation(req, 'update'); - if (!result.data) { - return sendAccepted(res); - } + const validator = new RegistryProjectValidator(); + const validatorAdapter = new RoutupContainerAdapter(validator); + const data = await validatorAdapter.run(req, { + group: HTTPHandlerOperation.CREATE, + }); const dataSource = await useDataSource(); const repository = dataSource.getRepository(RegistryProjectEntity); @@ -40,7 +42,7 @@ export async function updateRegistryProjectRouteHandler(req: Request, res: Respo throw new ForbiddenError(); } - entity = repository.merge(entity, result.data); + entity = repository.merge(entity, data); await repository.save(entity); @@ -49,16 +51,16 @@ export async function updateRegistryProjectRouteHandler(req: Request, res: Respo if ( entity.external_name && - result.data.external_name && - entity.external_name !== result.data.external_name + data.external_name && + entity.external_name !== data.external_name ) { await client.publish(buildRegistryTaskQueueRouterPayload({ command: RegistryCommand.PROJECT_UNLINK, data: { id: entity.id, registryId: entity.registry_id, - externalName: result.data.external_name, - accountId: result.data.account_id, + externalName: data.external_name, + accountId: data.account_id, }, })); } diff --git a/packages/server-core/src/http/controllers/registry-project/utils/index.ts b/packages/server-core/src/http/controllers/registry-project/utils/index.ts index 0a6d8bd4c..390661cca 100644 --- a/packages/server-core/src/http/controllers/registry-project/utils/index.ts +++ b/packages/server-core/src/http/controllers/registry-project/utils/index.ts @@ -5,4 +5,4 @@ * view the LICENSE file that was distributed with this source code. */ -export * from './validation'; +export * from './validator'; diff --git a/packages/server-core/src/http/controllers/registry-project/utils/validation.ts b/packages/server-core/src/http/controllers/registry-project/utils/validation.ts deleted file mode 100644 index 6ff2b9407..000000000 --- a/packages/server-core/src/http/controllers/registry-project/utils/validation.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2022-2024. - * Author Peter Placzek (tada5hi) - * For the full copyright and license information, - * view the LICENSE file that was distributed with this source code. - */ - -import { check } from 'express-validator'; -import { RegistryProjectType } from '@privateaim/core-kit'; -import type { Request } from 'routup'; -import type { HTTPValidationResult } from '@privateaim/server-http-kit'; -import { - createHTTPValidationResult, - extendHTTPValidationResultWithRelation, -} from '@privateaim/server-http-kit'; -import type { RegistryProjectEntity } from '../../../../domains'; -import { RegistryEntity } from '../../../../domains'; - -export async function runRegistryProjectValidation( - req: Request, - operation: 'create' | 'update', -) : Promise> { - const registryChain = check('registry_id') - .exists() - .isUUID(); - - if (operation === 'update') { - registryChain.optional(); - } - - await registryChain.run(req); - - // ---------------------------------------------- - - const titleChain = check('name') - .exists() - .isLength({ min: 5, max: 128 }); - - if (operation === 'update') { - titleChain.optional(); - } - - await titleChain.run(req); - - // ------------------------------------------------------------- - - const externalNameChain = check('external_name') - .isLength({ min: 1, max: 255 }) - .exists() - .matches(/^[a-z0-9-_]*$/); - - if (operation === 'update') { - externalNameChain.optional(); - } - - await externalNameChain.run(req); - - // ------------------------------------------------------------- - - if (operation === 'create') { - await check('type') - .exists() - .isIn(Object.values(RegistryProjectType)) - .run(req); - } - - // ---------------------------------------------- - - const result = createHTTPValidationResult(req); - await extendHTTPValidationResultWithRelation(result, RegistryEntity, { - id: 'registry_id', - entity: 'registry', - }); - - return result; -} diff --git a/packages/server-core/src/http/controllers/registry-project/utils/validator.ts b/packages/server-core/src/http/controllers/registry-project/utils/validator.ts new file mode 100644 index 000000000..1bcc1b5be --- /dev/null +++ b/packages/server-core/src/http/controllers/registry-project/utils/validator.ts @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2022-2024. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import { RegistryProjectType } from '@privateaim/core-kit'; +import { Container } from 'validup'; +import { createValidator } from '@validup/adapter-validator'; +import { HTTPHandlerOperation } from '@privateaim/server-http-kit'; +import type { RegistryProjectEntity } from '../../../../domains'; + +export class RegistryProjectValidator extends Container { + protected initialize() { + super.initialize(); + + this.mount( + 'registry_id', + { group: HTTPHandlerOperation.CREATE }, + createValidator((chain) => chain + .exists() + .notEmpty() + .isUUID()), + ); + + this.mount( + 'name', + { group: HTTPHandlerOperation.CREATE }, + createValidator((chain) => chain + .exists() + .isLength({ min: 5, max: 128 })), + ); + + this.mount( + 'name', + { group: HTTPHandlerOperation.UPDATE, optional: true }, + createValidator((chain) => chain + .exists() + .isLength({ min: 5, max: 128 }) + .optional({ values: 'null' })), + ); + + this.mount( + 'external_name', + { group: HTTPHandlerOperation.CREATE }, + createValidator((chain) => chain + .isLength({ min: 1, max: 255 }) + .exists() + .matches(/^[a-z0-9-_]*$/)), + ); + + this.mount( + 'external_name', + { group: HTTPHandlerOperation.UPDATE, optional: true }, + createValidator((chain) => chain + .isLength({ min: 1, max: 255 }) + .exists() + .matches(/^[a-z0-9-_]*$/) + .optional({ values: 'null' })), + ); + + this.mount( + 'type', + { group: HTTPHandlerOperation.CREATE }, + createValidator((chain) => chain + .exists() + .isIn(Object.values(RegistryProjectType))), + ); + } +} diff --git a/packages/server-core/src/http/controllers/registry/handlers/create.ts b/packages/server-core/src/http/controllers/registry/handlers/create.ts index b9e00b6da..afbb252b7 100644 --- a/packages/server-core/src/http/controllers/registry/handlers/create.ts +++ b/packages/server-core/src/http/controllers/registry/handlers/create.ts @@ -5,23 +5,30 @@ * view the LICENSE file that was distributed with this source code. */ -import { PermissionName } from '@privateaim/kit'; +import { PermissionName, getHostNameFromString } from '@privateaim/kit'; import type { Request, Response } from 'routup'; import { sendCreated } from 'routup'; import { useDataSource } from 'typeorm-extension'; -import { useRequestPermissionChecker } from '@privateaim/server-http-kit'; -import { runRegistryValidation } from '../utils'; +import { HTTPHandlerOperation, useRequestPermissionChecker } from '@privateaim/server-http-kit'; +import { RoutupContainerAdapter } from '@validup/adapter-routup'; +import { RegistryValidator } from '../utils'; import { RegistryEntity } from '../../../../domains'; export async function createRegistryRouteHandler(req: Request, res: Response) : Promise { const permissionChecker = useRequestPermissionChecker(req); await permissionChecker.preCheck({ name: PermissionName.REGISTRY_MANAGE }); - const result = await runRegistryValidation(req, 'create'); + const validator = new RegistryValidator(); + const validatorAdapter = new RoutupContainerAdapter(validator); + const data = await validatorAdapter.run(req, { + group: HTTPHandlerOperation.CREATE, + }); + + data.host = getHostNameFromString(data.host); const dataSource = await useDataSource(); const repository = dataSource.getRepository(RegistryEntity); - const entity = repository.create(result.data); + const entity = repository.create(data); await repository.save(entity); diff --git a/packages/server-core/src/http/controllers/registry/handlers/update.ts b/packages/server-core/src/http/controllers/registry/handlers/update.ts index d50bf0142..a99dc23eb 100644 --- a/packages/server-core/src/http/controllers/registry/handlers/update.ts +++ b/packages/server-core/src/http/controllers/registry/handlers/update.ts @@ -6,12 +6,13 @@ */ import { NotFoundError } from '@ebec/http'; -import { PermissionName } from '@privateaim/kit'; +import { PermissionName, getHostNameFromString } from '@privateaim/kit'; import type { Request, Response } from 'routup'; import { sendAccepted, useRequestParam } from 'routup'; import { useDataSource } from 'typeorm-extension'; -import { useRequestPermissionChecker } from '@privateaim/server-http-kit'; -import { runRegistryValidation } from '../utils'; +import { HTTPHandlerOperation, useRequestPermissionChecker } from '@privateaim/server-http-kit'; +import { RoutupContainerAdapter } from '@validup/adapter-routup'; +import { RegistryValidator } from '../utils'; import { RegistryEntity } from '../../../../domains'; export async function updateRegistryRouteHandler(req: Request, res: Response) : Promise { @@ -20,10 +21,12 @@ export async function updateRegistryRouteHandler(req: Request, res: Response) : const permissionChecker = useRequestPermissionChecker(req); await permissionChecker.preCheck({ name: PermissionName.REGISTRY_MANAGE }); - const result = await runRegistryValidation(req, 'update'); - if (!result.data) { - return sendAccepted(res); - } + const validator = new RegistryValidator(); + const validatorAdapter = new RoutupContainerAdapter(validator); + const data = await validatorAdapter.run(req, { + group: HTTPHandlerOperation.UPDATE, + }); + data.host = getHostNameFromString(data.host); const dataSource = await useDataSource(); const repository = dataSource.getRepository(RegistryEntity); @@ -33,7 +36,7 @@ export async function updateRegistryRouteHandler(req: Request, res: Response) : throw new NotFoundError(); } - entity = repository.merge(entity, result.data); + entity = repository.merge(entity, data); await repository.save(entity); diff --git a/packages/server-core/src/http/controllers/registry/utils/index.ts b/packages/server-core/src/http/controllers/registry/utils/index.ts index 0a6d8bd4c..390661cca 100644 --- a/packages/server-core/src/http/controllers/registry/utils/index.ts +++ b/packages/server-core/src/http/controllers/registry/utils/index.ts @@ -5,4 +5,4 @@ * view the LICENSE file that was distributed with this source code. */ -export * from './validation'; +export * from './validator'; diff --git a/packages/server-core/src/http/controllers/registry/utils/validation.ts b/packages/server-core/src/http/controllers/registry/utils/validation.ts deleted file mode 100644 index 848dea3d5..000000000 --- a/packages/server-core/src/http/controllers/registry/utils/validation.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (c) 2022-2024. - * Author Peter Placzek (tada5hi) - * For the full copyright and license information, - * view the LICENSE file that was distributed with this source code. - */ - -import { - createHTTPValidationResult, -} from '@privateaim/server-http-kit'; -import { body, check } from 'express-validator'; -import { getHostNameFromString } from '@privateaim/kit'; -import type { Request } from 'routup'; -import type { HTTPValidationResult } from '@privateaim/server-http-kit'; -import type { RegistryEntity } from '../../../../domains'; - -export async function runRegistryValidation( - req: Request, - operation: 'create' | 'update', -) : Promise> { - const titleChain = check('name') - .exists() - .isLength({ min: 3, max: 128 }); - - if (operation === 'update') { - titleChain.optional(); - } - - await titleChain.run(req); - - // ---------------------------------------------- - - const hostChain = body('host') - .exists() - .isString() - .isLength({ min: 3, max: 512 }); - - if (operation === 'update') { - hostChain.optional(); - } - - await hostChain.run(req); - - // ---------------------------------------------- - - await check('account_name') - .exists() - .isLength({ min: 3, max: 256 }) - .optional({ nullable: true }) - .run(req); - - // ---------------------------------------------- - - await check('account_secret') - .exists() - .isLength({ min: 3, max: 256 }) - .optional({ nullable: true }) - .run(req); - - // ---------------------------------------------- - - const result = createHTTPValidationResult(req); - if (result.data.host) { - result.data.host = getHostNameFromString(result.data.host); - } - - return result; -} diff --git a/packages/server-core/src/http/controllers/registry/utils/validator.ts b/packages/server-core/src/http/controllers/registry/utils/validator.ts new file mode 100644 index 000000000..269f0e13a --- /dev/null +++ b/packages/server-core/src/http/controllers/registry/utils/validator.ts @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2022-2024. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import { Container } from 'validup'; +import { createValidator } from '@validup/adapter-validator'; +import { HTTPHandlerOperation } from '@privateaim/server-http-kit'; +import type { RegistryEntity } from '../../../../domains'; + +export class RegistryValidator extends Container { + protected initialize() { + super.initialize(); + + this.mount( + 'name', + { group: HTTPHandlerOperation.CREATE }, + createValidator((chain) => chain + .exists() + .isLength({ min: 3, max: 128 })), + ); + + this.mount( + 'name', + { group: HTTPHandlerOperation.UPDATE, optional: true }, + createValidator((chain) => chain + .exists() + .isLength({ min: 3, max: 128 }) + .optional({ values: 'null' })), + ); + + this.mount( + 'host', + { group: HTTPHandlerOperation.CREATE }, + createValidator((chain) => chain + .exists() + .isString() + .isLength({ min: 3, max: 512 })), + ); + + this.mount( + 'host', + { group: HTTPHandlerOperation.CREATE, optional: true }, + createValidator((chain) => chain + .exists() + .isString() + .isLength({ min: 3, max: 512 }) + .optional({ values: 'null' })), + ); + + this.mount( + 'account_name', + { optional: true }, + createValidator((chain) => chain + .exists() + .isLength({ min: 3, max: 256 }) + .optional({ nullable: true })), + ); + + this.mount( + 'account_secret', + { optional: true }, + createValidator((chain) => chain + .exists() + .isLength({ min: 3, max: 256 }) + .optional({ nullable: true })), + ); + } +} diff --git a/packages/server-core/src/http/controllers/service/handlers/registry/command.ts b/packages/server-core/src/http/controllers/service/handlers/registry/command.ts index bd94cc715..8277684f1 100644 --- a/packages/server-core/src/http/controllers/service/handlers/registry/command.ts +++ b/packages/server-core/src/http/controllers/service/handlers/registry/command.ts @@ -9,6 +9,7 @@ import { RegistryAPICommand, } from '@privateaim/core-kit'; import { PermissionName } from '@privateaim/kit'; +import { RoutupContainerAdapter } from '@validup/adapter-routup'; import type { Request, Response } from 'routup'; import { sendAccepted } from 'routup'; import { useDataSource } from 'typeorm-extension'; @@ -23,13 +24,15 @@ import { buildRegistryTaskQueueRouterPayload, } from '../../../../../components'; import { RegistryEntity, RegistryProjectEntity } from '../../../../../domains'; -import { runServiceRegistryValidation } from '../../utils/validation'; +import { ServiceRegistryValidator } from '../../utils/validation'; export async function handleRegistryCommandRouteHandler(req: Request, res: Response) : Promise { const permissionChecker = useRequestPermissionChecker(req); await permissionChecker.preCheck({ name: PermissionName.REGISTRY_MANAGE }); - const { data: result } = await runServiceRegistryValidation(req); + const validator = new ServiceRegistryValidator(); + const validatorAdapter = new RoutupContainerAdapter(validator); + const result = await validatorAdapter.run(req); if (!isQueueRouterUsable()) { return sendAccepted(res); diff --git a/packages/server-core/src/http/controllers/service/utils/validation.ts b/packages/server-core/src/http/controllers/service/utils/validation.ts index 332949cbe..42c1da739 100644 --- a/packages/server-core/src/http/controllers/service/utils/validation.ts +++ b/packages/server-core/src/http/controllers/service/utils/validation.ts @@ -6,10 +6,8 @@ */ import { RegistryAPICommand } from '@privateaim/core-kit'; -import type { HTTPValidationResult } from '@privateaim/server-http-kit'; -import { createHTTPValidationResult } from '@privateaim/server-http-kit'; -import { check } from 'express-validator'; -import type { Request } from 'routup'; +import { createValidator } from '@validup/adapter-validator'; +import { Container } from 'validup'; type ValidationResult = { id: string, @@ -17,27 +15,29 @@ type ValidationResult = { secret?: string }; -export async function runServiceRegistryValidation( - req: Request, -) : Promise> { - await check('id') - .exists() - .notEmpty() - .isUUID() - .run(req); +export class ServiceRegistryValidator extends Container { + protected initialize() { + super.initialize(); - await check('command') - .exists() - .isString() - .custom((value) => Object.values(RegistryAPICommand).includes(value)) - .run(req); + this.mount( + 'id', + createValidator((chain) => chain + .isUUID()), + ); - await check('secret') - .matches(/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d).{8,}$/) - .optional({ nullable: true }) - .run(req); + this.mount( + 'command', + createValidator((chain) => chain + .isString() + .custom((value) => Object.values(RegistryAPICommand).includes(value))), + ); - // ---------------------------------------------- - - return createHTTPValidationResult(req); + this.mount( + 'secret', + { optional: true }, + createValidator((chain) => chain + .matches(/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d).{8,}$/) + .optional({ nullable: true })), + ); + } } diff --git a/packages/server-core/test/unit/http/analysis.spec.ts b/packages/server-core/test/unit/http/analysis.spec.ts index 22d74befb..354e6389e 100644 --- a/packages/server-core/test/unit/http/analysis.spec.ts +++ b/packages/server-core/test/unit/http/analysis.spec.ts @@ -7,8 +7,8 @@ import type { Analysis } from '@privateaim/core-kit'; import { createNanoID } from '@privateaim/kit'; -import { buildHTTPValidationErrorMessage } from '@privateaim/server-http-kit'; import { isClientError } from 'hapic'; +import { EntityRelationLookupError } from 'typeorm-extension'; import { createTestSuite, expectPropertiesEqualToSrc, @@ -86,7 +86,7 @@ describe('src/controllers/core/analysis', () => { }); } catch (e) { if (isClientError(e)) { - expect(e.response.data.message).toEqual(buildHTTPValidationErrorMessage(['project_id'])); + expect(e.response.data.message).toEqual(EntityRelationLookupError.notFound('project', ['project_id']).message); return; } @@ -111,7 +111,7 @@ describe('src/controllers/core/analysis', () => { }); } catch (e) { if (isClientError(e)) { - expect(e.response.data.message).toEqual(buildHTTPValidationErrorMessage(['master_image_id'])); + expect(e.response.data.message).toEqual(EntityRelationLookupError.notFound('master_image', ['master_image_id']).message); return; } diff --git a/packages/server-core/test/utils/properties.ts b/packages/server-core/test/utils/properties.ts index d234b0352..a68c94b44 100644 --- a/packages/server-core/test/utils/properties.ts +++ b/packages/server-core/test/utils/properties.ts @@ -5,12 +5,18 @@ * view the LICENSE file that was distributed with this source code. */ +import { hasOwnProperty } from '@privateaim/kit'; + export function expectPropertiesEqualToSrc( src: Record, dest: Record, ) { const keys = Object.keys(src); for (let i = 0; i < keys.length; i++) { + if (!hasOwnProperty(dest, keys[i])) { + continue; + } + expect(dest[keys[i]]).toEqual(src[keys[i]]); } } diff --git a/packages/server-http-kit/package.json b/packages/server-http-kit/package.json index 39d7e18db..023d44ec8 100644 --- a/packages/server-http-kit/package.json +++ b/packages/server-http-kit/package.json @@ -26,17 +26,16 @@ "@routup/swagger": "^2.4.1", "cors": "^2.8.5", "hapic": "^2.5.1", - "locter": "^2.1.4" + "locter": "^2.1.4", + "validup": "^0.1.7" }, "devDependencies": { "@types/cors": "^2.8.17", - "express-validator": "^7.2.0", "redis-extension": "^2.0.1", "routup": "^4.0.1", "typeorm-extension": "^3.6.2" }, "peerDependencies": { - "express-validator": "^7.2.0", "redis-extension": "^2.0.1", "routup": "^4.0.1", "typeorm-extension": "^3.6.2" diff --git a/packages/server-http-kit/src/validation/index.ts b/packages/server-http-kit/src/constants.ts similarity index 55% rename from packages/server-http-kit/src/validation/index.ts rename to packages/server-http-kit/src/constants.ts index de3d2e8ab..d62de82ed 100644 --- a/packages/server-http-kit/src/validation/index.ts +++ b/packages/server-http-kit/src/constants.ts @@ -1,11 +1,11 @@ /* - * Copyright (c) 2021-2024. + * Copyright (c) 2024. * Author Peter Placzek (tada5hi) * For the full copyright and license information, * view the LICENSE file that was distributed with this source code. */ -export * from './message'; -export * from './module'; -export * from './type'; -export * from './result'; +export enum HTTPHandlerOperation { + CREATE = 'create', + UPDATE = 'update', +} diff --git a/packages/server-http-kit/src/index.ts b/packages/server-http-kit/src/index.ts index 7dcc1be96..29ac9f0a3 100644 --- a/packages/server-http-kit/src/index.ts +++ b/packages/server-http-kit/src/index.ts @@ -5,8 +5,8 @@ * view the LICENSE file that was distributed with this source code. */ +export * from './constants'; export * from './middlewares'; export * from './request'; export * from './services'; export * from './swagger'; -export * from './validation'; diff --git a/packages/server-http-kit/src/middlewares/error.ts b/packages/server-http-kit/src/middlewares/error.ts index 3b9e35d2b..1cb674220 100644 --- a/packages/server-http-kit/src/middlewares/error.ts +++ b/packages/server-http-kit/src/middlewares/error.ts @@ -11,6 +11,7 @@ import type { Router } from 'routup'; import { errorHandler } from 'routup'; import { useLogger } from '@privateaim/server-kit'; import { EntityRelationLookupError } from 'typeorm-extension'; +import { ValidupNestedError } from 'validup'; export function mountErrorMiddleware(router: Router) { router.use(errorHandler((error, req, res) => { @@ -43,6 +44,15 @@ export function mountErrorMiddleware(router: Router) { error.statusCode = 400; } + if (error.cause instanceof ValidupNestedError) { + error.expose = true; + error.statusCode = 400; + error.data = { + children: error.cause.children, + attributes: error.cause.children.map((child) => child.pathAbsolute), + }; + } + // catch and decorate some db errors :) switch (error.code) { case 'ER_DUP_ENTRY': diff --git a/packages/server-http-kit/src/validation/message.ts b/packages/server-http-kit/src/validation/message.ts deleted file mode 100644 index 6558f3013..000000000 --- a/packages/server-http-kit/src/validation/message.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright (c) 2024. - * Author Peter Placzek (tada5hi) - * For the full copyright and license information, - * view the LICENSE file that was distributed with this source code. - */ - -export function buildHTTPValidationErrorMessage< - T extends Record = Record, ->(name: keyof T | (keyof T)[]) { - const names = Array.isArray(name) ? name : [name]; - - if (names.length > 1) { - return `The parameters ${names.join(', ')} is invalid.`; - } - return `The parameter ${String(names[0])} is invalid.`; -} diff --git a/packages/server-http-kit/src/validation/module.ts b/packages/server-http-kit/src/validation/module.ts deleted file mode 100644 index 02700d987..000000000 --- a/packages/server-http-kit/src/validation/module.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2022-2024. - * Author Peter Placzek (tada5hi) - * For the full copyright and license information, - * view the LICENSE file that was distributed with this source code. - */ - -import { BadRequestError } from '@ebec/http'; -import type { Result, ValidationError } from 'express-validator'; -import { buildHTTPValidationErrorMessage } from './message'; - -export class HTTPValidationError extends BadRequestError { - constructor(validation: Result) { - const errors : ValidationError[] = validation.array(); - - const parameterNames = []; - for (let i = 0; i < errors.length; i++) { - const item = errors[i]; - - switch (item.type) { - case 'field': { - parameterNames.push(item.path); - break; - } - case 'alternative': { - parameterNames.push(item.nestedErrors.map( - ((el) => el.path), - ).join('|')); - break; - } - } - } - - let message : string; - - if (parameterNames.length > 0) { - message = buildHTTPValidationErrorMessage(Array.from(new Set(parameterNames))); - } else { - message = 'An unexpected validation error occurred.'; - } - - super({ - message, - data: { - errors, - }, - }); - } -} diff --git a/packages/server-http-kit/src/validation/result.ts b/packages/server-http-kit/src/validation/result.ts deleted file mode 100644 index 01cb712b8..000000000 --- a/packages/server-http-kit/src/validation/result.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (c) 2022-2024. - * Author Peter Placzek (tada5hi) - * For the full copyright and license information, - * view the LICENSE file that was distributed with this source code. - */ - -import { BadRequestError } from '@ebec/http'; -import { deleteUndefinedObjectProperties } from '@privateaim/kit'; -import type { MatchedDataOptions } from 'express-validator'; -import { matchedData, validationResult } from 'express-validator'; -import type { Request } from 'routup'; -import type { EntityTarget } from 'typeorm'; -import { useDataSource } from 'typeorm-extension'; -import { buildHTTPValidationErrorMessage } from './message'; -import { HTTPValidationError } from './module'; -import type { HTTPValidationExtendKeys, HTTPValidationResult } from './type'; - -export function createHTTPValidationResult< - R extends Record, - M extends Record = Record, ->(req: Request, options: Partial = {}) : HTTPValidationResult { - const validation = validationResult(req); - if (!validation.isEmpty()) { - throw new HTTPValidationError(validation); - } - - return { - data: deleteUndefinedObjectProperties(matchedData(req, { - includeOptionals: true, - ...options, - })) as R, - meta: {} as M, - relation: {}, - }; -} - -export async function extendHTTPValidationResultWithRelation< - R extends Record, - >( - result: HTTPValidationResult, - target: EntityTarget, - keys: HTTPValidationExtendKeys, -) : Promise> { - if (result.data[keys.id]) { - const dataSource = await useDataSource(); - - const repository = dataSource.getRepository(target); - const entity = await repository.findOneBy({ id: result.data[keys.id] }); - if (!entity) { - throw new BadRequestError(buildHTTPValidationErrorMessage(keys.id)); - } - - result.relation[keys.entity as keyof HTTPValidationResult['relation']] = entity; - } - - return result; -} diff --git a/packages/server-http-kit/src/validation/type.ts b/packages/server-http-kit/src/validation/type.ts deleted file mode 100644 index 54aa9e914..000000000 --- a/packages/server-http-kit/src/validation/type.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2022-2024. - * Author Peter Placzek (tada5hi) - * For the full copyright and license information, - * view the LICENSE file that was distributed with this source code. - */ - -export type HTTPValidationResultMetaKey> = ({ - [K in keyof T]?: T[K] extends Record ? - T[K] extends Date ? never : K : - never -})[keyof T]; - -export type HTTPValidationResult< - T extends Record, - M extends Record = Record, - > = { - data: Partial, - relation: { - [K in HTTPValidationResultMetaKey]?: T[K] - }, - meta: M - }; - -export type HTTPValidationExtendKeys> = { - id: keyof T, - entity: HTTPValidationResultMetaKey -}; diff --git a/packages/server-kit/package.json b/packages/server-kit/package.json index d97dbaa92..3a251487d 100644 --- a/packages/server-kit/package.json +++ b/packages/server-kit/package.json @@ -42,14 +42,14 @@ "@hapic/vault": "^2.3.4", "amqp-extension": "^4.0.0-beta.3", "redis-extension": "^2.0.1", - "typeorm-extension": "^3.6.2" + "typeorm-extension": "^3.6.3" }, "peerDependencies": { "@authup/core-http-kit": "^1.0.0-beta.22", "@hapic/vault": "^2.3.4", "amqp-extension": "^4.0.0-beta.3", "redis-extension": "^2.0.1", - "typeorm-extension": "^3.6.2" + "typeorm-extension": "^3.6.3" }, "publishConfig": { "access": "public" diff --git a/packages/server-storage/package.json b/packages/server-storage/package.json index 84fb81335..2cc4c7b94 100644 --- a/packages/server-storage/package.json +++ b/packages/server-storage/package.json @@ -20,11 +20,12 @@ "@privateaim/storage-kit": "^0.8.4", "@routup/basic": "^1.4.1", "@routup/decorators": "^3.4.1", + "@validup/adapter-routup": "^0.1.8", + "@validup/adapter-validator": "^0.1.7", "busboy": "^1.6.0", "cors": "^2.8.5", "dotenv": "^16.4.4", "envix": "^1.3.0", - "express-validator": "^7.2.0", "hapic": "^2.5.1", "locter": "^2.1.4", "minio": "^8.0.2", @@ -34,7 +35,8 @@ "singa": "^1.0.0", "tar-stream": "^3.1.6", "typeorm": "^0.3.20", - "typeorm-extension": "^3.6.2" + "typeorm-extension": "^3.6.3", + "validup": "^0.1.7" }, "devDependencies": { "@types/busboy": "^1.5.3", diff --git a/packages/server-storage/src/http/controllers/bucket/handlers/create.ts b/packages/server-storage/src/http/controllers/bucket/handlers/create.ts index 6c81bd4d9..25c20b7ae 100644 --- a/packages/server-storage/src/http/controllers/bucket/handlers/create.ts +++ b/packages/server-storage/src/http/controllers/bucket/handlers/create.ts @@ -5,15 +5,22 @@ * view the LICENSE file that was distributed with this source code. */ +import { isRealmResourceWritable } from '@authup/core-kit'; +import { + HTTPHandlerOperation, + useRequestIdentityOrFail, + useRequestIdentityRealm, + useRequestPermissionChecker, +} from '@privateaim/server-http-kit'; import { PermissionName } from '@privateaim/kit'; import { ForbiddenError } from '@ebec/http'; +import { RoutupContainerAdapter } from '@validup/adapter-routup'; import type { Request, Response } from 'routup'; import { sendCreated } from 'routup'; import { useDataSource } from 'typeorm-extension'; -import { useRequestIdentityOrFail, useRequestPermissionChecker } from '@privateaim/server-http-kit'; import { useMinio } from '../../../../core'; import { BucketEntity, toBucketName } from '../../../../domains'; -import { runBucketValidation } from '../utils/validation'; +import { BucketValidator } from '../utils/validation'; export async function executeBucketRouteCreateHandler(req: Request, res: Response) : Promise { const permissionChecker = useRequestPermissionChecker(req); @@ -24,14 +31,27 @@ export async function executeBucketRouteCreateHandler(req: Request, res: Respons throw new ForbiddenError('Only users and robots are permitted to create a bucket.'); } - const result = await runBucketValidation(req, 'create'); + const validator = new BucketValidator(); + const validatorAdapter = new RoutupContainerAdapter(validator); + const data = await validatorAdapter.run(req, { + group: HTTPHandlerOperation.CREATE, + }); + + const realm = useRequestIdentityRealm(req); + if (data.realm_id) { + if (!isRealmResourceWritable(realm, data.realm_id)) { + throw new ForbiddenError('You are not permitted to create this bucket.'); + } + } else { + data.realm_id = realm.id; + } const dataSource = await useDataSource(); const repository = dataSource.getRepository(BucketEntity); const entity = repository.create({ actor_id: actor.id, actor_type: actor.type, - ...result.data, + ...data, }); await repository.save(entity); diff --git a/packages/server-storage/src/http/controllers/bucket/handlers/update.ts b/packages/server-storage/src/http/controllers/bucket/handlers/update.ts index d7d02c859..2e036da5e 100644 --- a/packages/server-storage/src/http/controllers/bucket/handlers/update.ts +++ b/packages/server-storage/src/http/controllers/bucket/handlers/update.ts @@ -9,10 +9,12 @@ import { isUUID } from '@authup/kit'; import { PermissionName } from '@privateaim/kit'; import { ForbiddenError, NotFoundError } from '@ebec/http'; import { isRealmResourceWritable } from '@authup/core-kit'; +import { RoutupContainerAdapter } from '@validup/adapter-routup'; import type { Request, Response } from 'routup'; import { sendAccepted, useRequestParam } from 'routup'; import { useDataSource } from 'typeorm-extension'; import { + HTTPHandlerOperation, useRequestIdentityOrFail, useRequestIdentityRealm, useRequestPermissionChecker, @@ -21,13 +23,14 @@ import { useMinio } from '../../../../core'; import { BucketEntity, isBucketOwnedByIdentity, toBucketName, } from '../../../../domains'; -import { runBucketValidation } from '../utils/validation'; +import { BucketValidator } from '../utils/validation'; export async function executeBucketRouteUpdateHandler(req: Request, res: Response) : Promise { - const result = await runBucketValidation(req, 'update'); - if (!result.data) { - return sendAccepted(res); - } + const validator = new BucketValidator(); + const validatorAdapter = new RoutupContainerAdapter(validator); + const data = await validatorAdapter.run(req, { + group: HTTPHandlerOperation.UPDATE, + }); const id = useRequestParam(req, 'id'); @@ -54,7 +57,7 @@ export async function executeBucketRouteUpdateHandler(req: Request, res: Respons } } - entity = repository.merge(entity, result.data); + entity = repository.merge(entity, data); await repository.save(entity); diff --git a/packages/server-storage/src/http/controllers/bucket/utils/validation.ts b/packages/server-storage/src/http/controllers/bucket/utils/validation.ts index eba3633d3..84e1b46bd 100644 --- a/packages/server-storage/src/http/controllers/bucket/utils/validation.ts +++ b/packages/server-storage/src/http/controllers/bucket/utils/validation.ts @@ -5,49 +5,31 @@ * view the LICENSE file that was distributed with this source code. */ -import { isRealmResourceWritable } from '@authup/core-kit'; -import { ForbiddenError } from '@ebec/http'; -import type { HTTPValidationResult } from '@privateaim/server-http-kit'; -import { createHTTPValidationResult, useRequestIdentityRealm } from '@privateaim/server-http-kit'; -import { check } from 'express-validator'; -import type { Request } from 'routup'; +import { HTTPHandlerOperation } from '@privateaim/server-http-kit'; +import { createValidator } from '@validup/adapter-validator'; +import { Container } from 'validup'; import type { BucketEntity } from '../../../../domains'; -export async function runBucketValidation( - req: Request, - operation: 'create' | 'update', -) : Promise> { - if (operation === 'create') { - const nameChain = check('name') - .exists() - .isLength({ min: 3, max: 256 }); - - await nameChain.run(req); - - const regionChain = check('region') - .exists() - .isLength({ min: 3, max: 256 }) - .optional(); - - await regionChain.run(req); +export class BucketValidator extends Container { + protected initialize() { + super.initialize(); + + this.mount( + 'name', + { group: HTTPHandlerOperation.CREATE }, + createValidator( + (chain) => chain.isLength({ min: 3, max: 256 }), + ), + ); + + this.mount( + 'region', + { group: HTTPHandlerOperation.CREATE, optional: true }, + createValidator( + (chain) => chain + .isLength({ min: 3, max: 256 }) + .optional({ values: 'null' }), + ), + ); } - - // ---------------------------------------------- - - const result = createHTTPValidationResult(req); - - // ---------------------------------------------- - - if (operation === 'create') { - const realm = useRequestIdentityRealm(req); - if (result.data.realm_id) { - if (!isRealmResourceWritable(realm, result.data.realm_id)) { - throw new ForbiddenError('You are not permitted to create this bucket.'); - } - } else { - result.data.realm_id = realm.id; - } - } - - return result; }