Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -39,15 +44,14 @@ 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:
- 3001:3000

db:
hostname: db
image: postgres:15.8-alpine
image: postgres:16.11-alpine
restart: on-failure
ports:
- 5432:5432
Expand Down
29 changes: 18 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,11 @@ S3_FILE_ITEM_ACCESS_KEY_ID=<your bucket key>
S3_FILE_ITEM_SECRET_ACCESS_KEY=<your bucket secret>

# 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=<your bucket key>
H5P_CONTENT_SECRET_ACCESS_KEY_ID=<your bucket secret>


### External services configuration
Expand Down Expand Up @@ -198,13 +199,9 @@ GRAASPER_CREATOR_ID=<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
Expand Down Expand Up @@ -283,23 +280,33 @@ garage layout assign -z dc1 -c 1G <node-id>
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
Expand Down
96 changes: 4 additions & 92 deletions src/services/auth/plugins/passport/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
5 changes: 0 additions & 5 deletions src/services/file/repositories/s3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion src/services/item/item.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
},
);

Expand Down
31 changes: 3 additions & 28 deletions src/services/item/item.schemas.packed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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'],
Expand All @@ -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 = {
Expand Down Expand Up @@ -164,7 +139,7 @@ export const getChildren = {
}),
),
response: {
[StatusCodes.OK]: Type.Array(extendedItemSchemaRef),
[StatusCodes.OK]: Type.Array(packedItemSchemaRef),
'4xx': errorSchemaRef,
},
} as const satisfies FastifySchema;
Expand Down
22 changes: 1 addition & 21 deletions src/services/item/plugins/html/h5p/h5p.schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' })),
}),
}),
},
Expand All @@ -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 = {
Expand Down
6 changes: 3 additions & 3 deletions src/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,17 +151,17 @@ 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');
}
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,
};

Expand Down
Loading