From 1a5befffa7b92162299abe042e44014805a86b85 Mon Sep 17 00:00:00 2001 From: tada5hi Date: Wed, 21 Feb 2024 13:36:25 +0100 Subject: [PATCH] feat: implemented bucket & bucket-file http controller --- package-lock.json | 9 +- .../core/src/domains/permission/constants.ts | 4 + packages/server-storage/package.json | 2 + .../server-storage/src/core/redis/module.ts | 4 + .../bucket-file/handlers/delete.ts | 44 ++++++++ .../controllers/bucket-file/handlers/index.ts | 9 ++ .../controllers/bucket-file/handlers/read.ts | 101 ++++++++++++++++++ .../src/http/controllers/bucket-file/index.ts | 49 +++++++++ .../controllers/bucket/handlers/create.ts | 36 +++++++ .../controllers/bucket/handlers/delete.ts | 44 ++++++++ .../http/controllers/bucket/handlers/index.ts | 11 ++ .../http/controllers/bucket/handlers/read.ts | 91 ++++++++++++++++ .../controllers/bucket/handlers/update.ts | 48 +++++++++ .../src/http/controllers/bucket/index.ts | 73 +++++++++++++ .../controllers/bucket/utils/validation.ts | 56 ++++++++++ .../src/http/controllers/index.ts | 9 ++ .../src/http/middlewares/auth.ts | 26 +++++ .../src/http/middlewares/authup.ts | 66 ++++++++++++ .../src/http/middlewares/index.ts | 9 ++ packages/server-storage/src/http/request.ts | 42 ++++++++ packages/server-storage/src/http/router.ts | 64 +++++++++++ packages/server-storage/src/http/server.ts | 9 +- 22 files changed, 799 insertions(+), 7 deletions(-) create mode 100644 packages/server-storage/src/http/controllers/bucket-file/handlers/delete.ts create mode 100644 packages/server-storage/src/http/controllers/bucket-file/handlers/index.ts create mode 100644 packages/server-storage/src/http/controllers/bucket-file/handlers/read.ts create mode 100644 packages/server-storage/src/http/controllers/bucket-file/index.ts create mode 100644 packages/server-storage/src/http/controllers/bucket/handlers/create.ts create mode 100644 packages/server-storage/src/http/controllers/bucket/handlers/delete.ts create mode 100644 packages/server-storage/src/http/controllers/bucket/handlers/index.ts create mode 100644 packages/server-storage/src/http/controllers/bucket/handlers/read.ts create mode 100644 packages/server-storage/src/http/controllers/bucket/handlers/update.ts create mode 100644 packages/server-storage/src/http/controllers/bucket/index.ts create mode 100644 packages/server-storage/src/http/controllers/bucket/utils/validation.ts create mode 100644 packages/server-storage/src/http/controllers/index.ts create mode 100644 packages/server-storage/src/http/middlewares/auth.ts create mode 100644 packages/server-storage/src/http/middlewares/authup.ts create mode 100644 packages/server-storage/src/http/middlewares/index.ts create mode 100644 packages/server-storage/src/http/request.ts create mode 100644 packages/server-storage/src/http/router.ts diff --git a/package-lock.json b/package-lock.json index fe6da144c..07513dc84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6526,7 +6526,6 @@ "version": "1.5.3", "resolved": "https://registry.npmjs.org/@types/busboy/-/busboy-1.5.3.tgz", "integrity": "sha512-YMBLFN/xBD8bnqywIlGyYqsNFXu6bsiY7h3Ae0kO17qEuTjsqeyYMRPSUDacIKIquws2Y6KjmxAyNx8xB3xQbw==", - "dev": true, "dependencies": { "@types/node": "*" } @@ -25940,6 +25939,7 @@ "typeorm-extension": "^3.5.0" }, "peerDependencies": { + "express-validator": "^7.0.1", "redis-extension": "^1.3.0", "typeorm-extension": "^3.5.0" } @@ -25964,6 +25964,7 @@ } }, "packages/server-storage": { + "name": "@privateaim/server-storage", "version": "0.0.0", "license": "Apache-2.0", "dependencies": { @@ -25971,12 +25972,18 @@ "@authup/server-adapter": "^1.0.0-beta.4", "@ebec/http": "^2.3.0", "@privateaim/core": "^0.1.0", + "@routup/basic": "^1.3.1", + "@routup/decorators": "^3.3.1", + "@types/busboy": "^1.5.3", + "busboy": "^1.6.0", "dotenv": "^16.4.4", "envix": "^1.3.0", "hapic": "^2.5.0", "redis-extension": "^1.3.0", "routup": "^3.2.0", "singa": "^1.0.0", + "typeorm": "^0.3.20", + "typeorm-extension": "^3.5.0", "winston": "^3.11.0" } }, diff --git a/packages/core/src/domains/permission/constants.ts b/packages/core/src/domains/permission/constants.ts index a5f89dc46..2be2f1d1d 100644 --- a/packages/core/src/domains/permission/constants.ts +++ b/packages/core/src/domains/permission/constants.ts @@ -10,6 +10,10 @@ import { PermissionName as AuthPermissionName } from '@authup/core'; export enum PermissionKey { ADMIN_UI_USE = 'admin_ui_use', + BUCKET_ADD = 'bucket_add', + BUCKET_EDIT = 'bucket_edit', + BUCKET_DROP = 'bucket_drop', + PROJECT_ADD = 'proposal_add', PROJECT_DROP = 'proposal_drop', PROJECT_EDIT = 'proposal_edit', diff --git a/packages/server-storage/package.json b/packages/server-storage/package.json index 6984bdd4b..743206098 100644 --- a/packages/server-storage/package.json +++ b/packages/server-storage/package.json @@ -14,6 +14,8 @@ "@authup/server-adapter": "^1.0.0-beta.4", "@ebec/http": "^2.3.0", "@privateaim/core": "^0.1.0", + "@routup/basic": "^1.3.1", + "@routup/decorators": "^3.3.1", "@types/busboy": "^1.5.3", "busboy": "^1.6.0", "dotenv": "^16.4.4", diff --git a/packages/server-storage/src/core/redis/module.ts b/packages/server-storage/src/core/redis/module.ts index 0b394bd9c..2aa34832f 100644 --- a/packages/server-storage/src/core/redis/module.ts +++ b/packages/server-storage/src/core/redis/module.ts @@ -17,6 +17,10 @@ export function useRedis() { return singleton.use(); } +export function hasRedis() { + return singleton.has() || singleton.hasFactory(); +} + export function setRedisFactory(factory: Factory) { return singleton.setFactory(factory); } diff --git a/packages/server-storage/src/http/controllers/bucket-file/handlers/delete.ts b/packages/server-storage/src/http/controllers/bucket-file/handlers/delete.ts new file mode 100644 index 000000000..ee01fb771 --- /dev/null +++ b/packages/server-storage/src/http/controllers/bucket-file/handlers/delete.ts @@ -0,0 +1,44 @@ +/* + * 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 { PermissionID } from '@privateaim/core'; +import { ForbiddenError, NotFoundError } from '@ebec/http'; +import { isRealmResourceWritable } from '@authup/core'; +import type { Request, Response } from 'routup'; +import { sendAccepted, useRequestParam } from 'routup'; +import { useDataSource } from 'typeorm-extension'; +import { BucketFileEntity } from '../../../../domains'; +import { useRequestEnv } from '../../../request'; + +export async function executeBucketFileRouteDeleteHandler(req: Request, res: Response) : Promise { + const id = useRequestParam(req, 'id'); + + const ability = useRequestEnv(req, 'ability'); + if (!ability.has(PermissionID.BUCKET_DROP)) { + throw new ForbiddenError(); + } + + const dataSource = await useDataSource(); + const repository = dataSource.getRepository(BucketFileEntity); + const entity = await repository.findOneBy({ id }); + + if (!entity) { + throw new NotFoundError(); + } + + if (!isRealmResourceWritable(useRequestEnv(req, 'realm'), entity.realm_id)) { + throw new ForbiddenError(); + } + + const { id: entityId } = entity; + + await repository.remove(entity); + + entity.id = entityId; + + return sendAccepted(res, entity); +} diff --git a/packages/server-storage/src/http/controllers/bucket-file/handlers/index.ts b/packages/server-storage/src/http/controllers/bucket-file/handlers/index.ts new file mode 100644 index 000000000..bc7afb784 --- /dev/null +++ b/packages/server-storage/src/http/controllers/bucket-file/handlers/index.ts @@ -0,0 +1,9 @@ +/* + * 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 * from './delete'; +export * from './read'; diff --git a/packages/server-storage/src/http/controllers/bucket-file/handlers/read.ts b/packages/server-storage/src/http/controllers/bucket-file/handlers/read.ts new file mode 100644 index 000000000..da6a65ed1 --- /dev/null +++ b/packages/server-storage/src/http/controllers/bucket-file/handlers/read.ts @@ -0,0 +1,101 @@ +/* + * 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 { useRequestQuery } from '@routup/basic/query'; +import type { Request, Response } from 'routup'; +import { send, useRequestParam } from 'routup'; +import { + applyQuery, + useDataSource, +} from 'typeorm-extension'; +import { NotFoundError } from '@ebec/http'; +import { BucketFileEntity } from '../../../../domains'; + +export async function executeBucketFileRouteGetOneHandler(req: Request, res: Response) : Promise { + const id = useRequestParam(req, 'id'); + + const dataSource = await useDataSource(); + const repository = dataSource.getRepository(BucketFileEntity); + const query = repository.createQueryBuilder('bucket') + .where('bucketFile.id = :id', { id }); + + applyQuery(query, useRequestQuery(req), { + defaultAlias: 'bucketFile', + fields: { + default: [ + 'id', + 'name', + 'directory', + 'size', + 'hash', + 'created_at', + 'updated_at', + 'realm_id', + 'robot_id', + 'user_id', + ], + }, + relations: { + allowed: ['bucket'], + }, + }); + + const entity = await query.getOne(); + + if (!entity) { + throw new NotFoundError(); + } + + return send(res, entity); +} + +export async function executeBucketFileRouteGetManyHandler(req: Request, res: Response) : Promise { + const dataSource = await useDataSource(); + + const repository = dataSource.getRepository(BucketFileEntity); + const query = repository.createQueryBuilder('bucket'); + + const { pagination } = applyQuery(query, useRequestQuery(req), { + defaultAlias: 'bucket', + fields: { + default: [ + 'id', + 'name', + 'directory', + 'size', + 'hash', + 'created_at', + 'updated_at', + 'realm_id', + 'robot_id', + 'user_id', + ], + }, + relations: { + allowed: ['bucket'], + }, + filters: { + allowed: ['id', 'name', 'directory', 'realm_id', 'user_id', 'robot_id'], + }, + pagination: { + maxLimit: 50, + }, + sort: { + allowed: ['id', 'directory', 'name', 'updated_at', 'created_at'], + }, + }); + + const [entities, total] = await query.getManyAndCount(); + + return send(res, { + data: entities, + meta: { + total, + ...pagination, + }, + }); +} diff --git a/packages/server-storage/src/http/controllers/bucket-file/index.ts b/packages/server-storage/src/http/controllers/bucket-file/index.ts new file mode 100644 index 000000000..7a1c84ba8 --- /dev/null +++ b/packages/server-storage/src/http/controllers/bucket-file/index.ts @@ -0,0 +1,49 @@ +/* + * 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 { + DController, DDelete, DGet, DPath, DRequest, DResponse, DTags, +} from '@routup/decorators'; +import type { BucketFileEntity } from '../../../domains'; +import { ForceLoggedInMiddleware } from '../../middlewares'; +import { + executeBucketFileRouteDeleteHandler, + executeBucketFileRouteGetManyHandler, + executeBucketFileRouteGetOneHandler, +} from './handlers'; + +type PartialBucketFile = Partial; + +@DTags('buckets') +@DController('/bucket-files') +export class BucketFileController { + @DGet('', [ForceLoggedInMiddleware]) + async getMany( + @DRequest() req: any, + @DResponse() res: any, + ): Promise { + return await executeBucketFileRouteGetManyHandler(req, res) as PartialBucketFile[]; + } + + @DGet('/:id', [ForceLoggedInMiddleware]) + async getOne( + @DPath('id') id: string, + @DRequest() req: any, + @DResponse() res: any, + ): Promise { + return await executeBucketFileRouteGetOneHandler(req, res) as PartialBucketFile | undefined; + } + + @DDelete('/:id', [ForceLoggedInMiddleware]) + async drop( + @DPath('id') id: string, + @DRequest() req: any, + @DResponse() res: any, + ): Promise { + return await executeBucketFileRouteDeleteHandler(req, res) as PartialBucketFile | undefined; + } +} diff --git a/packages/server-storage/src/http/controllers/bucket/handlers/create.ts b/packages/server-storage/src/http/controllers/bucket/handlers/create.ts new file mode 100644 index 000000000..0efaf32f5 --- /dev/null +++ b/packages/server-storage/src/http/controllers/bucket/handlers/create.ts @@ -0,0 +1,36 @@ +/* + * 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 { PermissionID } from '@privateaim/core'; +import { ForbiddenError } from '@ebec/http'; +import type { Request, Response } from 'routup'; +import { sendCreated } from 'routup'; +import { useDataSource } from 'typeorm-extension'; +import { useRequestEnv } from '../../../request'; +import { BucketEntity } from '../../../../domains'; +import { runProjectValidation } from '../utils/validation'; + +export async function executeBucketRouteCreateHandler(req: Request, res: Response) : Promise { + const ability = useRequestEnv(req, 'ability'); + if (!ability.has(PermissionID.BUCKET_ADD)) { + throw new ForbiddenError(); + } + + const result = await runProjectValidation(req, 'create'); + + const dataSource = await useDataSource(); + const repository = dataSource.getRepository(BucketEntity); + const entity = repository.create({ + user_id: useRequestEnv(req, 'userId'), + robot_id: useRequestEnv(req, 'robotId'), + ...result.data, + }); + + await repository.save(entity); + + return sendCreated(res, entity); +} diff --git a/packages/server-storage/src/http/controllers/bucket/handlers/delete.ts b/packages/server-storage/src/http/controllers/bucket/handlers/delete.ts new file mode 100644 index 000000000..c74303fed --- /dev/null +++ b/packages/server-storage/src/http/controllers/bucket/handlers/delete.ts @@ -0,0 +1,44 @@ +/* + * 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 { PermissionID } from '@privateaim/core'; +import { BadRequestError, ForbiddenError, NotFoundError } from '@ebec/http'; +import { isRealmResourceWritable } from '@authup/core'; +import type { Request, Response } from 'routup'; +import { sendAccepted, useRequestParam } from 'routup'; +import { useDataSource } from 'typeorm-extension'; +import { BucketEntity } from '../../../../domains'; +import { useRequestEnv } from '../../../request'; + +export async function executeBucketRouteDeleteHandler(req: Request, res: Response) : Promise { + const id = useRequestParam(req, 'id'); + + const ability = useRequestEnv(req, 'ability'); + if (!ability.has(PermissionID.BUCKET_DROP)) { + throw new ForbiddenError(); + } + + const dataSource = await useDataSource(); + const repository = dataSource.getRepository(BucketEntity); + const entity = await repository.findOneBy({ id }); + + if (!entity) { + throw new NotFoundError(); + } + + if (!isRealmResourceWritable(useRequestEnv(req, 'realm'), entity.realm_id)) { + throw new ForbiddenError(); + } + + const { id: entityId } = entity; + + await repository.remove(entity); + + entity.id = entityId; + + return sendAccepted(res, entity); +} diff --git a/packages/server-storage/src/http/controllers/bucket/handlers/index.ts b/packages/server-storage/src/http/controllers/bucket/handlers/index.ts new file mode 100644 index 000000000..68b6ddfb1 --- /dev/null +++ b/packages/server-storage/src/http/controllers/bucket/handlers/index.ts @@ -0,0 +1,11 @@ +/* + * 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 * from './create'; +export * from './delete'; +export * from './read'; +export * from './update'; diff --git a/packages/server-storage/src/http/controllers/bucket/handlers/read.ts b/packages/server-storage/src/http/controllers/bucket/handlers/read.ts new file mode 100644 index 000000000..6e5ac5c0a --- /dev/null +++ b/packages/server-storage/src/http/controllers/bucket/handlers/read.ts @@ -0,0 +1,91 @@ +/* + * 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 { useRequestQuery } from '@routup/basic/query'; +import type { Request, Response } from 'routup'; +import { send, useRequestParam } from 'routup'; +import { + applyQuery, + useDataSource, +} from 'typeorm-extension'; +import { NotFoundError } from '@ebec/http'; +import { BucketEntity } from '../../../../domains'; + +export async function executeBucketRouteGetOneHandler(req: Request, res: Response) : Promise { + const id = useRequestParam(req, 'id'); + + const dataSource = await useDataSource(); + const repository = dataSource.getRepository(BucketEntity); + const query = repository.createQueryBuilder('bucket') + .where('bucket.id = :id', { id }); + + applyQuery(query, useRequestQuery(req), { + defaultAlias: 'bucket', + fields: { + default: [ + 'id', + 'name', + 'region', + 'created_at', + 'updated_at', + 'realm_id', + 'robot_id', + 'user_id', + ], + }, + }); + + const entity = await query.getOne(); + + if (!entity) { + throw new NotFoundError(); + } + + return send(res, entity); +} + +export async function executeBucketRouteGetManyHandler(req: Request, res: Response) : Promise { + const dataSource = await useDataSource(); + + const repository = dataSource.getRepository(BucketEntity); + const query = repository.createQueryBuilder('bucket'); + + const { pagination } = applyQuery(query, useRequestQuery(req), { + defaultAlias: 'bucket', + fields: { + default: [ + 'id', + 'name', + 'region', + 'created_at', + 'updated_at', + 'realm_id', + 'robot_id', + 'user_id', + ], + }, + filters: { + allowed: ['id', 'name', 'realm_id', 'user_id', 'robot_id'], + }, + pagination: { + maxLimit: 50, + }, + sort: { + allowed: ['id', 'updated_at', 'created_at'], + }, + }); + + const [entities, total] = await query.getManyAndCount(); + + return send(res, { + data: entities, + meta: { + total, + ...pagination, + }, + }); +} diff --git a/packages/server-storage/src/http/controllers/bucket/handlers/update.ts b/packages/server-storage/src/http/controllers/bucket/handlers/update.ts new file mode 100644 index 000000000..e6ed87a8f --- /dev/null +++ b/packages/server-storage/src/http/controllers/bucket/handlers/update.ts @@ -0,0 +1,48 @@ +/* + * 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 { PermissionID } from '@privateaim/core'; +import { ForbiddenError, NotFoundError } from '@ebec/http'; +import { isRealmResourceWritable } from '@authup/core'; +import type { Request, Response } from 'routup'; +import { sendAccepted, useRequestParam } from 'routup'; +import { useDataSource } from 'typeorm-extension'; +import { BucketEntity } from '../../../../domains'; +import { useRequestEnv } from '../../../request'; +import { runProjectValidation } from '../utils/validation'; + +export async function executeBucketRouteUpdateHandler(req: Request, res: Response) : Promise { + const id = useRequestParam(req, 'id'); + + const ability = useRequestEnv(req, 'ability'); + if (!ability.has(PermissionID.BUCKET_EDIT)) { + throw new ForbiddenError(); + } + + const result = await runProjectValidation(req, 'update'); + if (!result.data) { + return sendAccepted(res); + } + + const dataSource = await useDataSource(); + const repository = dataSource.getRepository(BucketEntity); + let entity = await repository.findOneBy({ id }); + + if (!entity) { + throw new NotFoundError(); + } + + if (!isRealmResourceWritable(useRequestEnv(req, 'realm'), entity.realm_id)) { + throw new ForbiddenError(); + } + + entity = repository.merge(entity, result.data); + + await repository.save(entity); + + return sendAccepted(res, entity); +} diff --git a/packages/server-storage/src/http/controllers/bucket/index.ts b/packages/server-storage/src/http/controllers/bucket/index.ts new file mode 100644 index 000000000..d3bd21938 --- /dev/null +++ b/packages/server-storage/src/http/controllers/bucket/index.ts @@ -0,0 +1,73 @@ +/* + * 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 type { + Project, +} from '@privateaim/core'; +import { + DBody, DController, DDelete, DGet, DPath, DPost, DRequest, DResponse, DTags, +} from '@routup/decorators'; +import { BucketEntity } from '../../../domains'; +import { ForceLoggedInMiddleware } from '../../middlewares'; +import { + executeBucketRouteCreateHandler, + executeBucketRouteDeleteHandler, + executeBucketRouteGetManyHandler, + executeBucketRouteGetOneHandler, + executeBucketRouteUpdateHandler, +} from './handlers'; + +type PartialBucket = Partial; + +@DTags('buckets') +@DController('/buckets') +export class BucketController { + @DGet('', [ForceLoggedInMiddleware]) + async getMany( + @DRequest() req: any, + @DResponse() res: any, + ): Promise { + return await executeBucketRouteGetManyHandler(req, res) as PartialBucket[]; + } + + @DGet('/:id', [ForceLoggedInMiddleware]) + async getOne( + @DPath('id') id: string, + @DRequest() req: any, + @DResponse() res: any, + ): Promise { + return await executeBucketRouteGetOneHandler(req, res) as PartialBucket | undefined; + } + + @DPost('/:id', [ForceLoggedInMiddleware]) + async update( + @DPath('id') id: string, + @DBody() data: BucketEntity, + @DRequest() req: any, + @DResponse() res: any, + ): Promise { + return await executeBucketRouteUpdateHandler(req, res) as PartialBucket | undefined; + } + + @DPost('', [ForceLoggedInMiddleware]) + async add( + @DBody() data: BucketEntity, + @DRequest() req: any, + @DResponse() res: any, + ): Promise { + return await executeBucketRouteCreateHandler(req, res) as PartialBucket | undefined; + } + + @DDelete('/:id', [ForceLoggedInMiddleware]) + async drop( + @DPath('id') id: string, + @DRequest() req: any, + @DResponse() res: any, + ): Promise { + return await executeBucketRouteDeleteHandler(req, res) as PartialBucket | undefined; + } +} diff --git a/packages/server-storage/src/http/controllers/bucket/utils/validation.ts b/packages/server-storage/src/http/controllers/bucket/utils/validation.ts new file mode 100644 index 000000000..aa475fc0c --- /dev/null +++ b/packages/server-storage/src/http/controllers/bucket/utils/validation.ts @@ -0,0 +1,56 @@ +/* + * 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'; +import { ForbiddenError } from '@ebec/http'; +import type { HTTPValidationResult } from '@privateaim/server-kit/src'; +import { createHTTPValidationResult } from '@privateaim/server-kit/src'; +import { check } from 'express-validator'; +import type { Request } from 'routup'; +import type { BucketEntity } from '../../../../domains'; +import { useRequestEnv } from '../../../request'; + +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('master_image_id') + .isUUID() + .optional({ nullable: true }) + .run(req); + + // ---------------------------------------------- + + const result = createHTTPValidationResult(req); + + // ---------------------------------------------- + + if (operation === 'create') { + const realm = useRequestEnv(req, 'realm'); + 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; +} diff --git a/packages/server-storage/src/http/controllers/index.ts b/packages/server-storage/src/http/controllers/index.ts new file mode 100644 index 000000000..f558f5577 --- /dev/null +++ b/packages/server-storage/src/http/controllers/index.ts @@ -0,0 +1,9 @@ +/* + * 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 './bucket'; +export * from './bucket-file'; diff --git a/packages/server-storage/src/http/middlewares/auth.ts b/packages/server-storage/src/http/middlewares/auth.ts new file mode 100644 index 000000000..eaffe4cae --- /dev/null +++ b/packages/server-storage/src/http/middlewares/auth.ts @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2021-2024. + * Author Peter Placzek (tada5hi) + * For the full copyright and license information, + * view the LICENSE file that was distributed with this source code. + */ + +import { UnauthorizedError } from '@ebec/http'; +import type { HandlerInterface } from '@routup/decorators'; +import type { + Next, Request, Response, +} from 'routup'; +import { useRequestEnv } from '../request'; + +export class ForceLoggedInMiddleware implements HandlerInterface { + public run(request: Request, response: Response, next: Next) { + if ( + typeof useRequestEnv(request, 'userId') === 'undefined' && + typeof useRequestEnv(request, 'robotId') === 'undefined' + ) { + throw new UnauthorizedError(); + } + + next(); + } +} diff --git a/packages/server-storage/src/http/middlewares/authup.ts b/packages/server-storage/src/http/middlewares/authup.ts new file mode 100644 index 000000000..5c3b1e92f --- /dev/null +++ b/packages/server-storage/src/http/middlewares/authup.ts @@ -0,0 +1,66 @@ +/* + * 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 { AbilityManager } from '@authup/core'; +import { createHTTPMiddleware } from '@authup/server-adapter'; +import type { TokenVerifierRedisCacheOptions } from '@authup/server-adapter'; +import { useRequestCookie } from '@routup/basic/cookie'; +import { coreHandler } from 'routup'; +import type { Router } from 'routup'; +import { useEnv } from '../../config'; +import { hasRedis, useRedis } from '../../core'; +import { setRequestEnv } from '../request'; + +export function mountAuthupMiddleware(router: Router) { + let tokenCache : TokenVerifierRedisCacheOptions | undefined; + if (hasRedis()) { + tokenCache = { + type: 'redis', + client: useRedis(), + }; + } + + const middleware = createHTTPMiddleware({ + tokenByCookie: (req, cookieName) => useRequestCookie(req, cookieName), + tokenVerifier: { + baseURL: useEnv('authupApiURL'), + // todo: this should be robot/vault strategy + creator: { + type: 'user', + name: 'admin', + password: 'start123', + }, + cache: tokenCache, + }, + tokenVerifierHandler: (req, data) => { + const ability = new AbilityManager(data.permissions); + setRequestEnv(req, 'ability', ability); + + setRequestEnv(req, 'realmId', data.realm_id); + setRequestEnv(req, 'realmName', data.realm_name); + setRequestEnv(req, 'realm', { + id: data.realm_id, + name: data.realm_name, + }); + + switch (data.sub_kind) { + case 'user': { + setRequestEnv(req, 'userId', data.sub); + setRequestEnv(req, 'userName', data.sub_name); + break; + } + case 'robot': { + setRequestEnv(req, 'robotId', data.sub); + setRequestEnv(req, 'robotName', data.sub_name); + break; + } + } + }, + }); + + router.use(coreHandler((req, res, next) => middleware(req, res, next))); +} diff --git a/packages/server-storage/src/http/middlewares/index.ts b/packages/server-storage/src/http/middlewares/index.ts new file mode 100644 index 000000000..79554fa4d --- /dev/null +++ b/packages/server-storage/src/http/middlewares/index.ts @@ -0,0 +1,9 @@ +/* + * 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 './auth'; +export * from './authup'; diff --git a/packages/server-storage/src/http/request.ts b/packages/server-storage/src/http/request.ts new file mode 100644 index 000000000..433c5fc47 --- /dev/null +++ b/packages/server-storage/src/http/request.ts @@ -0,0 +1,42 @@ +/* + * 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 { AbilityManager } from '@authup/core'; +import { setRequestEnv as setEnv, useRequestEnv as useEnv } from 'routup'; +import type { Request } from 'routup'; + +type RequestEnv = { + ability?: AbilityManager, + + realmId?: string, + realmName?: string, + realm?: { id?: string, name?: string }, + + userId?: string, + userName?: string, + + robotId?: string, + robotName?: string +}; + +export function useRequestEnv(req: Request) : RequestEnv; +export function useRequestEnv(req: Request, key: T) : RequestEnv[T]; +export function useRequestEnv(req: Request, key?: T) { + if (typeof key === 'string') { + return useEnv(req, key) as RequestEnv[T]; + } + + return useEnv(req); +} + +export function setRequestEnv( + req: Request, + key: T, + value: RequestEnv[T], +) { + return setEnv(req, key, value); +} diff --git a/packages/server-storage/src/http/router.ts b/packages/server-storage/src/http/router.ts new file mode 100644 index 000000000..03f1d1dbe --- /dev/null +++ b/packages/server-storage/src/http/router.ts @@ -0,0 +1,64 @@ +/* + * 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 { decorators } from '@routup/decorators'; +import { + useRequestBody, +} from '@routup/basic/body'; +import { + useRequestCookie, + useRequestCookies, +} from '@routup/basic/cookie'; +import { + useRequestQuery, +} from '@routup/basic/query'; + +import { Router, coreHandler } from 'routup'; +import { BucketController, BucketFileController } from './controllers'; +import { mountAuthupMiddleware } from './middlewares'; + +export function createHTTPRouter() : Router { + const router = new Router(); + + mountAuthupMiddleware(router); + + router.get('/', coreHandler(() => ({ + timestamp: Date.now(), + }))); + + router.use(decorators({ + controllers: [ + BucketController, + BucketFileController, + ], + parameter: { + body: (context, name) => { + if (name) { + return useRequestBody(context.request, name); + } + + return useRequestBody(context.request); + }, + cookie: (context, name) => { + if (name) { + return useRequestCookie(context.request, name); + } + + return useRequestCookies(context.request); + }, + query: (context, name) => { + if (name) { + return useRequestQuery(context.request, name); + } + + return useRequestQuery(context.request); + }, + }, + })); + + return router; +} diff --git a/packages/server-storage/src/http/server.ts b/packages/server-storage/src/http/server.ts index 3f4c2d573..b4da5a82a 100644 --- a/packages/server-storage/src/http/server.ts +++ b/packages/server-storage/src/http/server.ts @@ -7,13 +7,10 @@ import type { Server } from 'node:http'; import http from 'node:http'; -import { Router, coreHandler, createNodeDispatcher } from 'routup'; +import { createNodeDispatcher } from 'routup'; +import { createHTTPRouter } from './router'; export function createHttpServer() : Server { - const router = new Router(); - router.get('/', coreHandler(() => ({ - timestamp: Date.now(), - }))); - + const router = createHTTPRouter(); return new http.Server(createNodeDispatcher(router)); }