diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index fe1016ef2..31f2f3a66 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -13,8 +13,9 @@ services: environment: # sane default to allow the server to bind to any interface in the container HOSTNAME: localhost - # H5P local storage host used in local only - H5P_FILE_STORAGE_HOST: http://localfile:1081 + # H5P storage using s3 api via garage + H5P_FILE_STORAGE_HOST: http://s3.garage.localhost:3900 + H5P_INTEGRATION_URL: http://h5p-items.web.garage.localhost:3902/h5p-integration/ # endpoint of the nudenet model IMAGE_CLASSIFIER_API: http://nudenet:8080/infer # the DB config is set by the "db" service below @@ -31,6 +32,10 @@ services: MAILER_CONNECTION: smtp://docker:docker@mailer:1025 # the Garage config is set by the "garage" service below S3_FILE_ITEM_HOST: http://s3.garage.localhost:3900 + # file storage options use s3 by default + FILE_STORAGE_TYPE: s3 + S3_FILE_ITEM_REGION: garage + S3_FILE_ITEM_BUCKET: file-items # the Iframely config is set by the "iframely" service below EMBEDDED_LINK_ITEM_IFRAMELY_HREF_ORIGIN: http://iframely:8061 # Redirection for shortlinks @@ -39,7 +44,6 @@ services: - garage:s3.garage.localhost volumes: - ..:/workspace:cached - - ../tmp:/tmp/graasp-file-item-storage # Overrides default command so things don't shut down after the process ends. command: sleep infinity ports: @@ -47,7 +51,7 @@ services: db: hostname: db - image: postgres:15.8-alpine + image: postgres:16.11-alpine restart: on-failure ports: - 5432:5432 diff --git a/README.md b/README.md index 2ab3e219e..c0d619fc1 100644 --- a/README.md +++ b/README.md @@ -166,10 +166,11 @@ S3_FILE_ITEM_ACCESS_KEY_ID= S3_FILE_ITEM_SECRET_ACCESS_KEY= # Graasp H5P -H5P_FILE_STORAGE_TYPE=local -H5P_STORAGE_ROOT_PATH=/tmp/graasp-h5p/ -H5P_PATH_PREFIX=h5p-content/ -H5P_FILE_STORAGE_HOST=http://localhost:1081 +H5P_FILE_STORAGE_TYPE=s3 +H5P_CONTENT_REGION=garage +H5P_CONTENT_BUCKET=h5p-items +H5P_CONTENT_ACCESS_KEY_ID= +H5P_CONTENT_SECRET_ACCESS_KEY_ID= ### External services configuration @@ -198,13 +199,9 @@ GRAASPER_CREATOR_ID= # redis[s]://[[username][:password]@][host][:port][/db-number]: # REDIS_CONNECTION= -# Graasp Actions -SAVE_ACTIONS=true - # Client hosts CLIENT_HOST=http://localhost:3114 LIBRARY_CLIENT_HOST=http://localhost:3005 -GRAASP_MOBILE_BUILDER=graasp-mobile-builder # Base url used to redirect shortlink aliases # SHORT_LINK_BASE_URL=http://localhost:3000/short-links @@ -283,23 +280,33 @@ garage layout assign -z dc1 -c 1G garage layout apply --version 1 ``` -Create a bucket +Create a bucket for the files and a bucket for the h5p items: ```sh garage bucket create file-items +garage bucket create h5p-items garage bucket list garage bucket info file-items +garage bucket info h5p-items ``` -Create an access key. Make not of the secret key as it will not be shown again ! +Create an access key. Make note of the secret key as it will not be shown again ! ```sh garage key create core-s3-key -# allow the key to access the bucket +# allow the key to access the files bucket garage bucket allow --read --write --owner file-items --key core-s3-key +# allow the key to access the h5p bucket +garage bucket allow --read --write --owner h5p-items --key core-s3-key +``` + +Lastly we will expose the h5p-items bucket as a website so it can be consumed from the internet (public): + +```sh +garage bucket website --allow h5p-items ``` ### Umami diff --git a/src/services/auth/plugins/passport/plugin.test.ts b/src/services/auth/plugins/passport/plugin.test.ts index eec06bf52..87070e513 100644 --- a/src/services/auth/plugins/passport/plugin.test.ts +++ b/src/services/auth/plugins/passport/plugin.test.ts @@ -96,16 +96,6 @@ describe('Passport Plugin', () => { expect(handler).toHaveBeenCalledTimes(1); expect(response.statusCode).toBe(StatusCodes.OK); }); - // it('Unknown JWT Member', async () => { - // const token = sign({ sub: v4() }, AUTH_TOKEN_JWT_SECRET); - // handler.mockImplementation(shouldBeNull); - // const response = await app.inject({ - // path: MOCKED_ROUTE, - // headers: { authorization: `Bearer ${token}` }, - // }); - // expect(handler).toHaveBeenCalledTimes(0); - // expect(response.statusCode).toBe(StatusCodes.NOT_FOUND); - // }); it('Invalid JWT Member', async () => { const { actor } = await seedFromJson(); assertIsDefined(actor); @@ -118,21 +108,7 @@ describe('Passport Plugin', () => { expect(handler).toHaveBeenCalledTimes(1); expect(response.statusCode).toBe(StatusCodes.OK); }); - // it('Valid JWT Member', async () => { - // const { actor } = await seedFromJson(); - // assertIsDefined(actor); - // assertIsMemberForTest(actor); - // const token = sign({ sub: actor.id }, AUTH_TOKEN_JWT_SECRET); - // handler.mockImplementation(({ user }) => { - // expect(user.account.id).toEqual(actor.id); - // }); - // const response = await app.inject({ - // path: MOCKED_ROUTE, - // headers: { authorization: `Bearer ${token}` }, - // }); - // expect(handler).toHaveBeenCalledTimes(1); - // expect(response.statusCode).toBe(StatusCodes.OK); - // }); + it('Invalid Session Member', async () => { const cookie = 'session=abc; Domain=localhost; Path=/; HttpOnly'; handler.mockImplementation(shouldBeNull); @@ -170,16 +146,7 @@ describe('Passport Plugin', () => { expect(handler).toHaveBeenCalledTimes(0); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); }); - // it('Unknown JWT Member', async () => { - // const token = sign({ sub: v4() }, AUTH_TOKEN_JWT_SECRET); - // handler.mockImplementation(shouldNotBeCalled); - // const response = await app.inject({ - // path: MOCKED_ROUTE, - // headers: { authorization: `Bearer ${token}` }, - // }); - // expect(handler).toHaveBeenCalledTimes(0); - // expect(response.statusCode).toBe(StatusCodes.NOT_FOUND); - // }); + it('Invalid JWT Member', async () => { const { actor } = await seedFromJson(); assertIsDefined(actor); @@ -192,19 +159,7 @@ describe('Passport Plugin', () => { expect(handler).toHaveBeenCalledTimes(0); expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); }); - // it('Valid JWT Member', async () => { - // const { actor } = await seedFromJson(); - // assertIsDefined(actor); - // assertIsMemberForTest(actor); - // const token = sign({ sub: actor.id }, AUTH_TOKEN_JWT_SECRET); - // handler.mockImplementation(({ user }) => expect(user.account.id).toEqual(actor.id)); - // const response = await app.inject({ - // path: MOCKED_ROUTE, - // headers: { authorization: `Bearer ${token}` }, - // }); - // expect(handler).toHaveBeenCalledTimes(1); - // expect(response.statusCode).toBe(StatusCodes.OK); - // }); + it('Invalid Session Member', async () => { const cookie = 'session=abc; Domain=localhost; Path=/; HttpOnly'; handler.mockImplementation(shouldNotBeCalled); @@ -339,50 +294,7 @@ describe('Passport Plugin', () => { expect(response.statusCode).toBe(StatusCodes.OK); }); }); - // describe('authenticateMobileMagicLink', () => { - // beforeEach(async () => { - // preHandler.mockImplementation(authenticateMobileMagicLink); - // }); - // it('Unauthenticated', async () => { - // handler.mockImplementation(shouldNotBeCalled); - // const response = await app.inject({ path: MOCKED_ROUTE }); - // expect(handler).toHaveBeenCalledTimes(0); - // expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); - // }); - // it('Unknown JWT Member', async () => { - // const token = sign({ sub: v4() }, AUTH_TOKEN_JWT_SECRET); - // handler.mockImplementation(shouldNotBeCalled); - // const response = await app.inject({ - // path: MOCKED_ROUTE, - // query: { token }, - // }); - // expect(handler).toHaveBeenCalledTimes(0); - // expect(response.statusCode).toBe(StatusCodes.NOT_FOUND); - // }); - // it('Invalid JWT Member', async () => { - // const token = sign({ sub: v4() }, 'invalid'); - // handler.mockImplementation(shouldNotBeCalled); - // const response = await app.inject({ - // path: MOCKED_ROUTE, - // query: { token }, - // }); - // expect(handler).toHaveBeenCalledTimes(0); - // expect(response.statusCode).toBe(StatusCodes.UNAUTHORIZED); - // }); - // it('Valid JWT Member', async () => { - // const { actor } = await seedFromJson(); - // assertIsDefined(actor); - // assertIsMemberForTest(actor); - // const token = sign({ sub: actor.id }, AUTH_TOKEN_JWT_SECRET); - // handler.mockImplementation(({ user }) => expect(user.account.id).toEqual(actor.id)); - // const response = await app.inject({ - // path: MOCKED_ROUTE, - // query: { token }, - // }); - // expect(handler).toHaveBeenCalledTimes(1); - // expect(response.statusCode).toBe(StatusCodes.OK); - // }); - // }); + describe('authenticatePasswordReset', () => { let token: string; let uuid: string; diff --git a/src/services/file/repositories/s3.ts b/src/services/file/repositories/s3.ts index cd146d1d2..2b379b7eb 100644 --- a/src/services/file/repositories/s3.ts +++ b/src/services/file/repositories/s3.ts @@ -333,11 +333,6 @@ export class S3FileRepository implements FileRepository { try { await Promise.allSettled(uploads.map((upload) => upload.done())); - - console.debug( - 'Upload successfully at', - files.map((f) => f.filepath), - ); } catch (err) { console.error('Something went wrong:', err); throw new UploadFileUnexpectedError(err); diff --git a/src/services/item/item.controller.ts b/src/services/item/item.controller.ts index 228c4514c..466075e12 100644 --- a/src/services/item/item.controller.ts +++ b/src/services/item/item.controller.ts @@ -125,7 +125,8 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { preHandler: optionalIsAuthenticated, }, async ({ user, params: { id } }) => { - return itemService.getPacked(db, user?.account, id); + const packedItem = await itemService.getPacked(db, user?.account, id); + return packedItem; }, ); diff --git a/src/services/item/item.schemas.packed.ts b/src/services/item/item.schemas.packed.ts index 7f55777bd..9836e474e 100644 --- a/src/services/item/item.schemas.packed.ts +++ b/src/services/item/item.schemas.packed.ts @@ -14,7 +14,7 @@ import { embeddedLinkItemSchemaRef } from './plugins/embeddedLink/link.schemas'; import { etherpadItemSchemaRef } from './plugins/etherpad/etherpad.schemas'; import { fileItemSchemaRef } from './plugins/file/itemFile.schema'; import { folderItemSchemaRef } from './plugins/folder/folder.schemas'; -import { h5pExtendedItemSchema, h5pItemSchemaRef } from './plugins/html/h5p/h5p.schemas'; +import { h5pItemSchemaRef } from './plugins/html/h5p/h5p.schemas'; import { itemVisibilitySchemaRef } from './plugins/itemVisibility/itemVisibility.schemas'; import { pageItemSchemaRef } from './plugins/page/page.schemas'; import { shortcutItemSchemaRef } from './plugins/shortcut/shortcut.schemas'; @@ -74,31 +74,6 @@ export const packedItemSchemaRef = registerSchemaAsRef( ), ); -export const extendedItemSchemaRef = registerSchemaAsRef( - 'extendedItem', - 'Extended Item', - Type.Intersect( - [ - Type.Union([ - appItemSchemaRef, - documentItemSchemaRef, - embeddedLinkItemSchemaRef, - etherpadItemSchemaRef, - fileItemSchemaRef, - folderItemSchemaRef, - h5pExtendedItemSchema, - pageItemSchemaRef, - shortcutItemSchemaRef, - ]), - packedSchema, - ], - { - discriminator: 'type', - description: 'Item with extended information useful for complete display', - }, - ), -); - export const getOne = { operationId: 'getItem', tags: ['item'], @@ -108,7 +83,7 @@ export const getOne = { params: customType.StrictObject({ id: customType.UUID(), }), - response: { [StatusCodes.OK]: extendedItemSchemaRef, '4xx': errorSchemaRef }, + response: { [StatusCodes.OK]: packedItemSchemaRef, '4xx': errorSchemaRef }, } as const satisfies FastifySchema; export const getAccessible = { @@ -164,7 +139,7 @@ export const getChildren = { }), ), response: { - [StatusCodes.OK]: Type.Array(extendedItemSchemaRef), + [StatusCodes.OK]: Type.Array(packedItemSchemaRef), '4xx': errorSchemaRef, }, } as const satisfies FastifySchema; diff --git a/src/services/item/plugins/html/h5p/h5p.schemas.ts b/src/services/item/plugins/html/h5p/h5p.schemas.ts index 14b01cc54..9ed818fbd 100644 --- a/src/services/item/plugins/html/h5p/h5p.schemas.ts +++ b/src/services/item/plugins/html/h5p/h5p.schemas.ts @@ -19,6 +19,7 @@ const h5pItemSchema = Type.Composite([ contentId: Type.String(), h5pFilePath: Type.String(), contentFilePath: Type.String(), + integrationUrl: Type.Optional(Type.String({ description: 'url of the h5p integration' })), }), }), }, @@ -29,27 +30,6 @@ const h5pItemSchema = Type.Composite([ ), ]); -export const h5pExtendedItemSchema = Type.Composite([ - itemCommonSchema, - customType.StrictObject( - { - type: Type.Literal('h5p'), - extra: customType.StrictObject({ - h5p: customType.StrictObject({ - contentId: Type.String(), - h5pFilePath: Type.String(), - contentFilePath: Type.String(), - integrationUrl: Type.String({ description: 'url of the h5p integration' }), - }), - }), - }, - { - title: 'H5P Extended Item', - description: 'Extended item of type H5P.', - }, - ), -]); - export const h5pItemSchemaRef = registerSchemaAsRef('h5pItem', 'H5P Item', h5pItemSchema); export const h5pImport = { diff --git a/src/utils/config.ts b/src/utils/config.ts index b5a322660..3fc9c5c63 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -151,8 +151,8 @@ if (H5P_FILE_STORAGE_TYPE === FileStorage.S3) { if ( !process.env.H5P_CONTENT_REGION || !process.env.H5P_CONTENT_BUCKET || - !process.env.H5P_CONTENT_SECRET_ACCESS_KEY_ID || - !process.env.H5P_CONTENT_ACCESS_KEY_ID + !process.env.H5P_CONTENT_ACCESS_KEY_ID || + !process.env.H5P_CONTENT_SECRET_ACCESS_KEY_ID ) throw new Error('H5P S3 configuration missing'); } @@ -160,8 +160,8 @@ export const H5P_S3_CONFIG = { s3: { s3Region: process.env.H5P_CONTENT_REGION, s3Bucket: process.env.H5P_CONTENT_BUCKET, - s3SecretAccessKey: process.env.H5P_CONTENT_SECRET_ACCESS_KEY_ID, s3AccessKeyId: process.env.H5P_CONTENT_ACCESS_KEY_ID, + s3SecretAccessKey: process.env.H5P_CONTENT_SECRET_ACCESS_KEY_ID, } as S3FileConfiguration, };