From 25866c557fc88a56d9a10fdf57d9ca936170c554 Mon Sep 17 00:00:00 2001 From: Anders Rasmussen Date: Thu, 5 Oct 2023 08:10:32 +0200 Subject: [PATCH] #14 - Added item endpoint, to get item with extra data (Folder, Blob, etc.) --- src/locales/da.json | 4 + src/locales/en.json | 4 + src/modules/auth/auth.route.ts | 8 +- src/modules/item/blob/blob.route.ts | 32 +++++--- src/modules/item/folder/folder.controller.ts | 2 +- src/modules/item/folder/folder.route.ts | 32 +++++--- src/modules/item/index.ts | 8 ++ src/modules/item/item.controller.ts | 56 ++++++++++++++ src/modules/item/item.route.ts | 52 +++++++++++++ src/modules/item/item.schema.ts | 77 +++++++++++++++++++- src/modules/item/item.service.ts | 61 +++++++++++++++- src/modules/item/sharing/sharing.route.ts | 32 +++++--- src/plugins/swagger.ts | 8 ++ 13 files changed, 334 insertions(+), 42 deletions(-) create mode 100644 src/modules/item/item.controller.ts create mode 100644 src/modules/item/item.route.ts diff --git a/src/locales/da.json b/src/locales/da.json index 1f04e66..e6a13ce 100644 --- a/src/locales/da.json +++ b/src/locales/da.json @@ -79,6 +79,10 @@ "required": "id er påkrævet", "type": "id skal være et tal" }, + "parentId": { + "required": "Parent id er påkrævet", + "type": "Parent id skal være et tal" + }, "blob": { "notFound": "Blob ikke fundet" }, diff --git a/src/locales/en.json b/src/locales/en.json index 6d94b41..a921d5a 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -79,6 +79,10 @@ "required": "id is required", "type": "id must be a number" }, + "parentId": { + "required": "Parent id is required", + "type": "Parent id must be a number" + }, "blob": { "notFound": "Blob not found" }, diff --git a/src/modules/auth/auth.route.ts b/src/modules/auth/auth.route.ts index 5c7267b..1f1906c 100644 --- a/src/modules/auth/auth.route.ts +++ b/src/modules/auth/auth.route.ts @@ -66,13 +66,15 @@ export default async (fastify: FastifyInstance) => { '/user', { schema: { - headers: { - Authorization: true, - }, tags: ['Auth'], response: { 200: { $ref: 'userResponseSchema' }, }, + security: [ + { + bearerAuth: [], + }, + ], }, onRequest: [fastify.authenticate], }, diff --git a/src/modules/item/blob/blob.route.ts b/src/modules/item/blob/blob.route.ts index 5bffb80..84ae08a 100644 --- a/src/modules/item/blob/blob.route.ts +++ b/src/modules/item/blob/blob.route.ts @@ -15,9 +15,6 @@ export default async (fastify: FastifyInstance) => { '/:id', { schema: { - headers: { - Authorization: true, - }, tags: ['Blob'], params: { $ref: 'readBlobSchema' }, response: { @@ -25,6 +22,11 @@ export default async (fastify: FastifyInstance) => { $ref: 'readBlobResponseSchema', }, }, + security: [ + { + bearerAuth: [], + }, + ], }, onRequest: [fastify.authenticate], }, @@ -35,9 +37,6 @@ export default async (fastify: FastifyInstance) => { '/', { schema: { - headers: { - Authorization: true, - }, tags: ['Blob'], body: { $ref: 'editBlobSchema' }, response: { @@ -45,6 +44,11 @@ export default async (fastify: FastifyInstance) => { $ref: 'editBlobResponseSchema', }, }, + security: [ + { + bearerAuth: [], + }, + ], }, onRequest: [fastify.authenticate], }, @@ -55,14 +59,16 @@ export default async (fastify: FastifyInstance) => { '/', { schema: { - headers: { - Authorization: true, - }, tags: ['Blob'], body: { $ref: 'uploadBlobSchema' }, response: { 200: { $ref: 'uploadBlobResponseSchema' }, }, + security: [ + { + bearerAuth: [], + }, + ], }, }, blobController.addHandler.bind(blobController), @@ -72,11 +78,13 @@ export default async (fastify: FastifyInstance) => { '/:id', { schema: { - headers: { - Authorization: true, - }, tags: ['Blob'], params: { $ref: 'deleteBlobSchema' }, + security: [ + { + bearerAuth: [], + }, + ], }, onRequest: [fastify.authenticate], }, diff --git a/src/modules/item/folder/folder.controller.ts b/src/modules/item/folder/folder.controller.ts index f2fa6da..027de54 100644 --- a/src/modules/item/folder/folder.controller.ts +++ b/src/modules/item/folder/folder.controller.ts @@ -104,7 +104,7 @@ export default class FolderController { return reply.unauthorized(); } - await this.folderService.deleteFolderByItemId(request.params.id); + await this.folderService.deleteFolderByItemId(folder.id); return reply.code(204).send(); } catch (e) { if (e instanceof Error) { diff --git a/src/modules/item/folder/folder.route.ts b/src/modules/item/folder/folder.route.ts index fdb0bb3..dfa5d24 100644 --- a/src/modules/item/folder/folder.route.ts +++ b/src/modules/item/folder/folder.route.ts @@ -16,14 +16,16 @@ export default async (fastify: FastifyInstance) => { '/:id', { schema: { - headers: { - Authorization: true, - }, tags: ['Folder'], params: { $ref: 'readFolderSchema' }, response: { 200: { $ref: 'readFolderResponseSchema' }, }, + security: [ + { + bearerAuth: [], + }, + ], }, onRequest: [fastify.authenticate], }, @@ -34,14 +36,16 @@ export default async (fastify: FastifyInstance) => { '/', { schema: { - headers: { - Authorization: true, - }, tags: ['Folder'], body: { $ref: 'editFolderSchema' }, response: { 200: { $ref: 'editFolderResponseSchema' }, }, + security: [ + { + bearerAuth: [], + }, + ], }, onRequest: [fastify.authenticate], }, @@ -52,14 +56,16 @@ export default async (fastify: FastifyInstance) => { '/', { schema: { - headers: { - Authorization: true, - }, tags: ['Folder'], body: { $ref: 'addFolderSchema' }, response: { 200: { $ref: 'addFolderResponseSchema' }, }, + security: [ + { + bearerAuth: [], + }, + ], }, onRequest: [fastify.authenticate], }, @@ -70,11 +76,13 @@ export default async (fastify: FastifyInstance) => { '/:id', { schema: { - headers: { - Authorization: true, - }, tags: ['Folder'], params: { $ref: 'deleteFolderSchema' }, + security: [ + { + bearerAuth: [], + }, + ], }, onRequest: [fastify.authenticate], }, diff --git a/src/modules/item/index.ts b/src/modules/item/index.ts index c09ee34..c86cfb1 100644 --- a/src/modules/item/index.ts +++ b/src/modules/item/index.ts @@ -4,9 +4,17 @@ import folder from './folder'; import { getOptionsWithPrefix } from '..'; import blob from './blob'; import sharing from './sharing'; +import { itemSchemas } from './item.schema'; +import itemRoute from './item.route'; export default fastifyPlugin(async (fastify: FastifyInstance, options: FastifyPluginOptions) => { await fastify.register(blob, getOptionsWithPrefix(options, '/blob')); await fastify.register(folder, getOptionsWithPrefix(options, '/folder')); await fastify.register(sharing, getOptionsWithPrefix(options, '/sharing')); + + for (const schema of itemSchemas) { + fastify.addSchema(schema); + } + + await fastify.register(itemRoute, options); }); diff --git a/src/modules/item/item.controller.ts b/src/modules/item/item.controller.ts new file mode 100644 index 0000000..16eb23b --- /dev/null +++ b/src/modules/item/item.controller.ts @@ -0,0 +1,56 @@ +import { FastifyReply, FastifyRequest } from 'fastify'; +import ItemService from './item.service'; +import { ReadInput } from './item.schema'; +import AccessService from './sharing/access.service'; + +export default class ItemController { + private itemService: ItemService; + private accessService: AccessService; + + constructor(itemService: ItemService, accessService: AccessService) { + this.itemService = itemService; + this.accessService = accessService; + } + + public async browseHandler(request: FastifyRequest, reply: FastifyReply) { + try { + const items = await this.itemService.getByOwnerIdAndParentId(request.user.sub, null); + + return reply.code(200).send(items); + } catch (e) { + if (e instanceof Error) { + return reply.badRequest(request.i18n.t(e.message)); + } + + /* istanbul ignore next */ + return reply.badRequest(); + } + } + + public async readHandler( + request: FastifyRequest<{ + Params: ReadInput; + }>, + reply: FastifyReply, + ) { + try { + if (!(await this.accessService.hasAccessToItem(request.params.parentId, request.user.sub))) { + return reply.unauthorized(); + } + + const items = await this.itemService.getByOwnerIdAndParentIdAndSharred( + request.user.sub, + request.params.parentId, + ); + + return reply.code(200).send(items); + } catch (e) { + if (e instanceof Error) { + return reply.badRequest(request.i18n.t(e.message)); + } + + /* istanbul ignore next */ + return reply.badRequest(); + } + } +} diff --git a/src/modules/item/item.route.ts b/src/modules/item/item.route.ts new file mode 100644 index 0000000..2e5821e --- /dev/null +++ b/src/modules/item/item.route.ts @@ -0,0 +1,52 @@ +import { FastifyInstance } from 'fastify'; +import ItemController from './item.controller'; +import ItemService from './item.service'; +import AccessService from './sharing/access.service'; +import SharingService from './sharing/sharing.service'; + +export default async (fastify: FastifyInstance) => { + const itemService = new ItemService(); + const itemController = new ItemController( + itemService, + new AccessService(itemService, new SharingService()), + ); + + fastify.get( + '/', + { + schema: { + tags: ['Items'], + response: { + 200: { $ref: 'itemsResponseSchema' }, + }, + security: [ + { + bearerAuth: [], + }, + ], + }, + onRequest: [fastify.authenticate], + }, + itemController.browseHandler.bind(itemController), + ); + + fastify.get( + '/:parentId', + { + schema: { + tags: ['Items'], + params: { $ref: 'readItemsSchema' }, + response: { + 200: { $ref: 'itemsResponseSchema' }, + }, + security: [ + { + bearerAuth: [], + }, + ], + }, + onRequest: [fastify.authenticate], + }, + itemController.readHandler.bind(itemController), + ); +}; diff --git a/src/modules/item/item.schema.ts b/src/modules/item/item.schema.ts index e480901..cab6451 100644 --- a/src/modules/item/item.schema.ts +++ b/src/modules/item/item.schema.ts @@ -1,7 +1,82 @@ -import { Item as prismaItemType } from '@prisma/client'; +import { + Item as prismaItemType, + ItemBlob as prismaItemBlobType, + ItemFolder as prismaItemFolderType, +} from '@prisma/client'; +import { FromSchema } from 'json-schema-to-ts'; export type Item = prismaItemType; +export type ItemPrismaProperties = Item & { ItemBlob: prismaItemBlobType | null } & { + ItemFolder: prismaItemFolderType | null; +}; +export type ItemWithProperties = Item & + Omit, 'id' | 'itemId'> & + Omit, 'id' | 'itemId'>; + export type CreateItem = Omit; export type UpdateItem = Pick & Partial; + +const readItemsSchema = { + $id: 'readItemsSchema', + type: 'object', + properties: { + parentId: { + type: 'number', + errorMessage: { + type: 'item.parentId.type', + }, + }, + }, + required: ['parentId'], + errorMessage: { + required: { + id: 'item.parentId.required', + }, + }, +} as const; + +const itemsResponseSchema = { + $id: 'itemsResponseSchema', + type: 'array', + items: { + type: 'object', + properties: { + id: { + type: 'number', + }, + name: { + type: 'string', + }, + color: { + type: ['string', 'null'], + }, + blobUrl: { + type: ['string', 'null'], + }, + parentId: { + type: ['number', 'null'], + }, + mimeType: { + type: 'string', + }, + ownerId: { + type: 'number', + }, + deletedAt: { + type: ['string', 'null'], + }, + createdAt: { + type: 'string', + }, + updatedAt: { + type: 'string', + }, + }, + }, +} as const; + +export type ReadInput = FromSchema; + +export const itemSchemas = [readItemsSchema, itemsResponseSchema]; diff --git a/src/modules/item/item.service.ts b/src/modules/item/item.service.ts index a23b599..81f3aea 100644 --- a/src/modules/item/item.service.ts +++ b/src/modules/item/item.service.ts @@ -1,5 +1,5 @@ import { prisma } from '../../plugins/prisma'; -import { CreateItem, Item } from './item.schema'; +import { CreateItem, Item, ItemPrismaProperties, ItemWithProperties } from './item.schema'; export default class ItemService { public async getById(id: number): Promise { @@ -28,4 +28,63 @@ export default class ItemService { return item; } + + public async getByOwnerIdAndParentId( + ownerId: number, + parentId: number | null, + ): Promise { + const items = await prisma.item.findMany({ + where: { + parentId: parentId, + ownerId: ownerId, + }, + include: { + ItemBlob: true, + ItemFolder: true, + }, + }); + + return this.formatItems(items); + } + + public async getByOwnerIdAndParentIdAndSharred( + userId: number, + parentId: number | null, + ): Promise { + const items = await prisma.item.findMany({ + where: { + parentId: parentId, + OR: [ + { + ownerId: userId, + }, + { + ItemSharing: { + some: { + userId, + }, + }, + }, + ], + }, + include: { + ItemBlob: true, + ItemFolder: true, + }, + }); + + return this.formatItems(items); + } + + private formatItems(items: ItemPrismaProperties[]): ItemWithProperties[] { + return items.map((element) => { + const { ItemFolder, ItemBlob, ...strippedElement } = element; + + return { + ...ItemBlob, + ...ItemFolder, + ...strippedElement, + }; + }); + } } diff --git a/src/modules/item/sharing/sharing.route.ts b/src/modules/item/sharing/sharing.route.ts index 67fe205..ddc389e 100644 --- a/src/modules/item/sharing/sharing.route.ts +++ b/src/modules/item/sharing/sharing.route.ts @@ -15,9 +15,6 @@ export default async (fastify: FastifyInstance) => { '/:id', { schema: { - headers: { - Authorization: true, - }, tags: ['Sharing'], params: { $ref: 'readSharingSchema' }, response: { @@ -25,6 +22,11 @@ export default async (fastify: FastifyInstance) => { $ref: 'readSharingResponseSchema', }, }, + security: [ + { + bearerAuth: [], + }, + ], }, onRequest: [fastify.authenticate], }, @@ -35,9 +37,6 @@ export default async (fastify: FastifyInstance) => { '/', { schema: { - headers: { - Authorization: true, - }, tags: ['Sharing'], body: { $ref: 'editSharingSchema' }, response: { @@ -45,6 +44,11 @@ export default async (fastify: FastifyInstance) => { $ref: 'editSharingResponseSchema', }, }, + security: [ + { + bearerAuth: [], + }, + ], }, onRequest: [fastify.authenticate], }, @@ -55,14 +59,16 @@ export default async (fastify: FastifyInstance) => { '/', { schema: { - headers: { - Authorization: true, - }, tags: ['Sharing'], body: { $ref: 'uploadSharingSchema' }, response: { 200: { $ref: 'uploadSharingResponseSchema' }, }, + security: [ + { + bearerAuth: [], + }, + ], }, onRequest: [fastify.authenticate], }, @@ -73,11 +79,13 @@ export default async (fastify: FastifyInstance) => { '/:id', { schema: { - headers: { - Authorization: true, - }, tags: ['Sharing'], params: { $ref: 'deleteSharingSchema' }, + security: [ + { + bearerAuth: [], + }, + ], }, onRequest: [fastify.authenticate], }, diff --git a/src/plugins/swagger.ts b/src/plugins/swagger.ts index c07f9d9..1a4cefa 100644 --- a/src/plugins/swagger.ts +++ b/src/plugins/swagger.ts @@ -19,6 +19,14 @@ export default fastifyPlugin( title: 'API', version: '1.0.0', }, + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + }, + }, + }, }, });