diff --git a/docs/content/dev/public_api.yml b/docs/content/dev/public_api.yml index f51d4ed11..0b9c8460f 100644 --- a/docs/content/dev/public_api.yml +++ b/docs/content/dev/public_api.yml @@ -900,19 +900,21 @@ components: MediaUpload: type: object properties: - note: - type: string - description: ID of the note the file was uploaded to - user: - type: string - description: username of the user who uploaded the file url: type: string description: URL of the file + owningNote: + type: string + description: ID of the note the file was uploaded to createdAt: type: string format: date-time description: Date when the file was upladed + owningUser: + type: string + description: username of the user who uploaded the file + + examples: markdownExample: value: '# Some header\nSome normal text. **Some bold text**' diff --git a/src/api/public/me/me.controller.spec.ts b/src/api/public/me/me.controller.spec.ts index 4f1daf4a0..e9e10a1e2 100644 --- a/src/api/public/me/me.controller.spec.ts +++ b/src/api/public/me/me.controller.spec.ts @@ -23,7 +23,10 @@ import { HistoryEntry } from '../../../history/history-entry.entity'; import { NoteGroupPermission } from '../../../permissions/note-group-permission.entity'; import { NoteUserPermission } from '../../../permissions/note-user-permission.entity'; import { Group } from '../../../groups/group.entity'; +import { MediaModule } from '../../../media/media.module'; +import { MediaUpload } from '../../../media/media-upload.entity'; import { ConfigModule } from '@nestjs/config'; +import mediaConfigMock from '../../../config/media.config.mock'; import appConfigMock from '../../../config/app.config.mock'; describe('Me Controller', () => { @@ -33,14 +36,19 @@ describe('Me Controller', () => { const module: TestingModule = await Test.createTestingModule({ controllers: [MeController], imports: [ - UsersModule, - HistoryModule, - NotesModule, - LoggerModule, + ConfigModule.forRoot({ + isGlobal: true, + load: [mediaConfigMock], + }), ConfigModule.forRoot({ isGlobal: true, load: [appConfigMock], }), + UsersModule, + HistoryModule, + NotesModule, + LoggerModule, + MediaModule, ], }) .overrideProvider(getRepositoryToken(User)) @@ -67,6 +75,8 @@ describe('Me Controller', () => { .useValue({}) .overrideProvider(getRepositoryToken(Group)) .useValue({}) + .overrideProvider(getRepositoryToken(MediaUpload)) + .useValue({}) .compile(); controller = module.get(MeController); diff --git a/src/api/public/me/me.controller.ts b/src/api/public/me/me.controller.ts index 1b9e73b5f..c42d9a4fa 100644 --- a/src/api/public/me/me.controller.ts +++ b/src/api/public/me/me.controller.ts @@ -28,6 +28,9 @@ import { HistoryEntryDto } from '../../../history/history-entry.dto'; import { UserInfoDto } from '../../../users/user-info.dto'; import { NotInDBError } from '../../../errors/errors'; import { Request } from 'express'; +import { MediaService } from '../../../media/media.service'; +import { MediaUploadUrlDto } from '../../../media/media-upload-url.dto'; +import { MediaUploadDto } from '../../../media/media-upload.dto'; @ApiTags('me') @ApiSecurity('token') @@ -38,6 +41,7 @@ export class MeController { private usersService: UsersService, private historyService: HistoryService, private notesService: NotesService, + private mediaService: MediaService, ) { this.logger.setContext(MeController.name); } @@ -129,4 +133,11 @@ export class MeController { (await notes).map((note) => this.notesService.toNoteMetadataDto(note)), ); } + + @UseGuards(TokenAuthGuard) + @Get('media') + async getMyMedia(@Req() req: Request): Promise { + const media = await this.mediaService.listUploadsByUser(req.user); + return media.map((media) => this.mediaService.toMediaUploadDto(media)); + } } diff --git a/src/media/media-upload.dto.ts b/src/media/media-upload.dto.ts new file mode 100644 index 000000000..6adf91836 --- /dev/null +++ b/src/media/media-upload.dto.ts @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { IsDate, IsString } from 'class-validator'; + +export class MediaUploadDto { + /** + * The link to the media file. + * @example "https://example.com/uploads/testfile123.jpg" + */ + @IsString() + url: string; + + /** + * The noteId of the note to which the uploaded file is linked to. + * @example "noteId" TODO how looks a note id? + */ + @IsString() + noteId: string; + + /** + * The date when the upload objects was created. + * @example "2020-12-01 12:23:34" + */ + @IsDate() + createdAt: Date; + + /** + * The userName of the user which uploaded the media file. + * @example "testuser5" + */ + @IsString() + userName: string; +} diff --git a/src/media/media.service.spec.ts b/src/media/media.service.spec.ts index d04017a79..a71464e81 100644 --- a/src/media/media.service.spec.ts +++ b/src/media/media.service.spec.ts @@ -229,4 +229,39 @@ describe('MediaService', () => { } }); }); + + describe('listUploadsByUser', () => { + describe('works', () => { + it('with one upload from user', async () => { + const mockMediaUploadEntry = { + id: 'testMediaUpload', + backendData: 'testBackendData', + user: { + userName: 'hardcoded', + } as User, + } as MediaUpload; + jest + .spyOn(mediaRepo, 'find') + .mockResolvedValueOnce([mockMediaUploadEntry]); + expect( + await service.listUploadsByUser({ userName: 'hardcoded' } as User), + ).toEqual([mockMediaUploadEntry]); + }); + + it('without uploads from user', async () => { + jest.spyOn(mediaRepo, 'find').mockResolvedValueOnce([]); + const mediaList = await service.listUploadsByUser({ + userName: 'hardcoded', + } as User); + expect(mediaList).toEqual([]); + }); + it('with error (undefined as return value of find)', async () => { + jest.spyOn(mediaRepo, 'find').mockResolvedValueOnce(undefined); + const mediaList = await service.listUploadsByUser({ + userName: 'hardcoded', + } as User); + expect(mediaList).toEqual([]); + }); + }); + }); }); diff --git a/src/media/media.service.ts b/src/media/media.service.ts index 910d4211d..303a732a1 100644 --- a/src/media/media.service.ts +++ b/src/media/media.service.ts @@ -22,6 +22,8 @@ import { MediaUploadUrlDto } from './media-upload-url.dto'; import { S3Backend } from './backends/s3-backend'; import { AzureBackend } from './backends/azure-backend'; import { ImgurBackend } from './backends/imgur-backend'; +import { User } from '../users/user.entity'; +import { MediaUploadDto } from './media-upload.dto'; @Injectable() export class MediaService { @@ -157,6 +159,23 @@ export class MediaService { return mediaUpload; } + /** + * @async + * List all uploads by a specific user + * @param {User} user - the specific user + * @return {MediaUpload[]} arary of media uploads owned by the user + */ + async listUploadsByUser(user: User): Promise { + const mediaUploads = await this.mediaUploadRepository.find({ + where: { user: user }, + relations: ['user', 'note'], + }); + if (mediaUploads === undefined) { + return []; + } + return mediaUploads; + } + private chooseBackendType(): BackendType { switch (this.mediaConfig.backend.use) { case 'filesystem': @@ -183,6 +202,15 @@ export class MediaService { } } + toMediaUploadDto(mediaUpload: MediaUpload): MediaUploadDto { + return { + url: mediaUpload.fileUrl, + noteId: mediaUpload.note.id, + createdAt: mediaUpload.createdAt, + userName: mediaUpload.user.userName, + }; + } + toMediaUploadUrlDto(url: string): MediaUploadUrlDto { return { link: url, diff --git a/test/public-api/me.e2e-spec.ts b/test/public-api/me.e2e-spec.ts index 15a03573c..788530b2a 100644 --- a/test/public-api/me.e2e-spec.ts +++ b/test/public-api/me.e2e-spec.ts @@ -33,6 +33,9 @@ import { ConfigModule } from '@nestjs/config'; import mediaConfigMock from '../../src/config/media.config.mock'; import appConfigMock from '../../src/config/app.config.mock'; import { User } from '../../src/users/user.entity'; +import { MediaService } from '../../src/media/media.service'; +import { MediaModule } from '../../src/media/media.module'; +import { promises as fs } from 'fs'; import { NoteMetadataDto } from '../../src/notes/note-metadata.dto'; // TODO Tests have to be reworked using UserService functions @@ -42,6 +45,7 @@ describe('Notes', () => { let historyService: HistoryService; let notesService: NotesService; let userService: UsersService; + let mediaService: MediaService; let user: User; beforeAll(async () => { @@ -66,6 +70,7 @@ describe('Notes', () => { AuthModule, UsersModule, HistoryModule, + MediaModule, ], }) .overrideGuard(TokenAuthGuard) @@ -75,6 +80,7 @@ describe('Notes', () => { notesService = moduleRef.get(NotesService); historyService = moduleRef.get(HistoryService); userService = moduleRef.get(UsersService); + mediaService = moduleRef.get(MediaService); user = await userService.createUser('hardcoded', 'Testy'); await app.init(); }); @@ -222,6 +228,41 @@ describe('Notes', () => { expect(noteMetaDtos[0].updateUser.userName).toEqual(user.userName); }); + it('GET /me/media', async () => { + const note1 = await notesService.createNote( + 'This is a test note.', + 'test8', + await userService.getUserByUsername('hardcoded'), + ); + const note2 = await notesService.createNote( + 'This is a test note.', + 'test9', + await userService.getUserByUsername('hardcoded'), + ); + const httpServer = app.getHttpServer(); + const response1 = await request(httpServer) + .get('/me/media/') + .expect('Content-Type', /json/) + .expect(200); + expect(response1.body).toHaveLength(0); + + const testImage = await fs.readFile('test/public-api/fixtures/test.png'); + const url0 = await mediaService.saveFile(testImage, 'hardcoded', note1.id); + const url1 = await mediaService.saveFile(testImage, 'hardcoded', note1.id); + const url2 = await mediaService.saveFile(testImage, 'hardcoded', note2.id); + const url3 = await mediaService.saveFile(testImage, 'hardcoded', note2.id); + + const response = await request(httpServer) + .get('/me/media/') + .expect('Content-Type', /json/) + .expect(200); + expect(response.body).toHaveLength(4); + expect(response.body[0].url).toEqual(url0); + expect(response.body[1].url).toEqual(url1); + expect(response.body[2].url).toEqual(url2); + expect(response.body[3].url).toEqual(url3); + }); + afterAll(async () => { await app.close(); });