diff --git a/src/api/public/me/me.controller.ts b/src/api/public/me/me.controller.ts index 02e51e2af..4bbf8ccdb 100644 --- a/src/api/public/me/me.controller.ts +++ b/src/api/public/me/me.controller.ts @@ -78,7 +78,10 @@ export class MeController { @UseGuards(TokenAuthGuard) @Get('notes') - getMyNotes(@Request() req): NoteMetadataDto[] { - return this.notesService.getUserNotes(req.user.userName); + async getMyNotes(@Request() req): Promise { + const notes = await this.notesService.getUserNotes(req.user); + return Promise.all( + notes.map((note) => this.notesService.toNoteMetadataDto(note)), + ); } } diff --git a/src/api/public/media/media.controller.ts b/src/api/public/media/media.controller.ts index f8fd3bb45..c1c211063 100644 --- a/src/api/public/media/media.controller.ts +++ b/src/api/public/media/media.controller.ts @@ -29,6 +29,7 @@ import { MediaService } from '../../../media/media.service'; import { MulterFile } from '../../../media/multer-file.interface'; import { TokenAuthGuard } from '../../../auth/token-auth.guard'; import { ApiSecurity } from '@nestjs/swagger'; +import { MediaUploadUrlDto } from '../../../media/media-upload-url.dto'; @ApiSecurity('token') @Controller('media') @@ -47,7 +48,7 @@ export class MediaController { @Request() req, @UploadedFile() file: MulterFile, @Headers('HedgeDoc-Note') noteId: string, - ) { + ): Promise { const username = req.user.userName; this.logger.debug( `Recieved filename '${file.originalname}' for note '${noteId}' from user '${username}'`, @@ -59,9 +60,7 @@ export class MediaController { username, noteId, ); - return { - link: url, - }; + return this.mediaService.toMediaUploadUrlDto(url); } catch (e) { if (e instanceof ClientError || e instanceof NotInDBError) { throw new BadRequestException(e.message); @@ -72,7 +71,10 @@ export class MediaController { @UseGuards(TokenAuthGuard) @Delete(':filename') - async deleteMedia(@Request() req, @Param('filename') filename: string) { + async deleteMedia( + @Request() req, + @Param('filename') filename: string, + ): Promise { const username = req.user.userName; try { await this.mediaService.deleteFile(filename, username); diff --git a/src/api/public/monitoring/monitoring.controller.ts b/src/api/public/monitoring/monitoring.controller.ts index f32e39701..d90b81117 100644 --- a/src/api/public/monitoring/monitoring.controller.ts +++ b/src/api/public/monitoring/monitoring.controller.ts @@ -8,6 +8,7 @@ import { Controller, Get, UseGuards } from '@nestjs/common'; import { MonitoringService } from '../../../monitoring/monitoring.service'; import { TokenAuthGuard } from '../../../auth/token-auth.guard'; import { ApiSecurity } from '@nestjs/swagger'; +import { ServerStatusDto } from '../../../monitoring/server-status.dto'; @ApiSecurity('token') @Controller('monitoring') @@ -16,7 +17,8 @@ export class MonitoringController { @UseGuards(TokenAuthGuard) @Get() - getStatus() { + getStatus(): Promise { + // TODO: toServerStatusDto. return this.monitoringService.getServerStatus(); } diff --git a/src/api/public/notes/notes.controller.ts b/src/api/public/notes/notes.controller.ts index 5a1782eb3..4478356d5 100644 --- a/src/api/public/notes/notes.controller.ts +++ b/src/api/public/notes/notes.controller.ts @@ -19,12 +19,19 @@ import { } from '@nestjs/common'; import { NotInDBError } from '../../../errors/errors'; import { ConsoleLoggerService } from '../../../logger/console-logger.service'; -import { NotePermissionsUpdateDto } from '../../../notes/note-permissions.dto'; +import { + NotePermissionsDto, + NotePermissionsUpdateDto, +} from '../../../notes/note-permissions.dto'; import { NotesService } from '../../../notes/notes.service'; import { RevisionsService } from '../../../revisions/revisions.service'; import { MarkdownBody } from '../../utils/markdownbody-decorator'; import { TokenAuthGuard } from '../../../auth/token-auth.guard'; import { ApiSecurity } from '@nestjs/swagger'; +import { NoteDto } from '../../../notes/note.dto'; +import { NoteMetadataDto } from '../../../notes/note-metadata.dto'; +import { RevisionMetadataDto } from '../../../revisions/revision-metadata.dto'; +import { RevisionDto } from '../../../revisions/revision.dto'; @ApiSecurity('token') @Controller('notes') @@ -39,18 +46,42 @@ export class NotesController { @UseGuards(TokenAuthGuard) @Post() - async createNote(@Request() req, @MarkdownBody() text: string) { + async createNote( + @Request() req, + @MarkdownBody() text: string, + ): Promise { // ToDo: provide user for createNoteDto this.logger.debug('Got raw markdown:\n' + text); - return this.noteService.createNoteDto(text); + return this.noteService.toNoteDto( + await this.noteService.createNote(text, undefined, req.user), + ); + } + + @UseGuards(TokenAuthGuard) + @Post(':noteAlias') + async createNamedNote( + @Request() req, + @Param('noteAlias') noteAlias: string, + @MarkdownBody() text: string, + ): Promise { + // ToDo: check if user is allowed to view this note + this.logger.debug('Got raw markdown:\n' + text); + return this.noteService.toNoteDto( + await this.noteService.createNote(text, noteAlias, req.user), + ); } @UseGuards(TokenAuthGuard) @Get(':noteIdOrAlias') - async getNote(@Request() req, @Param('noteIdOrAlias') noteIdOrAlias: string) { + async getNote( + @Request() req, + @Param('noteIdOrAlias') noteIdOrAlias: string, + ): Promise { // ToDo: check if user is allowed to view this note try { - return await this.noteService.getNoteDtoByIdOrAlias(noteIdOrAlias); + return this.noteService.toNoteDto( + await this.noteService.getNoteByIdOrAlias(noteIdOrAlias), + ); } catch (e) { if (e instanceof NotInDBError) { throw new NotFoundException(e.message); @@ -59,24 +90,12 @@ export class NotesController { } } - @UseGuards(TokenAuthGuard) - @Post(':noteAlias') - async createNamedNote( - @Request() req, - @Param('noteAlias') noteAlias: string, - @MarkdownBody() text: string, - ) { - // ToDo: check if user is allowed to view this note - this.logger.debug('Got raw markdown:\n' + text); - return this.noteService.createNoteDto(text, noteAlias); - } - @UseGuards(TokenAuthGuard) @Delete(':noteIdOrAlias') async deleteNote( @Request() req, @Param('noteIdOrAlias') noteIdOrAlias: string, - ) { + ): Promise { // ToDo: check if user is allowed to delete this note this.logger.debug('Deleting note: ' + noteIdOrAlias); try { @@ -97,11 +116,13 @@ export class NotesController { @Request() req, @Param('noteIdOrAlias') noteIdOrAlias: string, @MarkdownBody() text: string, - ) { + ): Promise { // ToDo: check if user is allowed to change this note this.logger.debug('Got raw markdown:\n' + text); try { - return await this.noteService.updateNoteByIdOrAlias(noteIdOrAlias, text); + return this.noteService.toNoteDto( + await this.noteService.updateNoteByIdOrAlias(noteIdOrAlias, text), + ); } catch (e) { if (e instanceof NotInDBError) { throw new NotFoundException(e.message); @@ -116,7 +137,7 @@ export class NotesController { async getNoteContent( @Request() req, @Param('noteIdOrAlias') noteIdOrAlias: string, - ) { + ): Promise { // ToDo: check if user is allowed to view this notes content try { return await this.noteService.getNoteContent(noteIdOrAlias); @@ -133,10 +154,12 @@ export class NotesController { async getNoteMetadata( @Request() req, @Param('noteIdOrAlias') noteIdOrAlias: string, - ) { + ): Promise { // ToDo: check if user is allowed to view this notes metadata try { - return await this.noteService.getNoteMetadata(noteIdOrAlias); + return this.noteService.toNoteMetadataDto( + await this.noteService.getNoteByIdOrAlias(noteIdOrAlias), + ); } catch (e) { if (e instanceof NotInDBError) { throw new NotFoundException(e.message); @@ -151,12 +174,11 @@ export class NotesController { @Request() req, @Param('noteIdOrAlias') noteIdOrAlias: string, @Body() updateDto: NotePermissionsUpdateDto, - ) { + ): Promise { // ToDo: check if user is allowed to view this notes permissions try { - return await this.noteService.updateNotePermissions( - noteIdOrAlias, - updateDto, + return this.noteService.toNotePermissionsDto( + await this.noteService.updateNotePermissions(noteIdOrAlias, updateDto), ); } catch (e) { if (e instanceof NotInDBError) { @@ -171,12 +193,17 @@ export class NotesController { async getNoteRevisions( @Request() req, @Param('noteIdOrAlias') noteIdOrAlias: string, - ) { + ): Promise { // ToDo: check if user is allowed to view this notes revisions try { - return await this.revisionsService.getNoteRevisionMetadatas( + const revisions = await this.revisionsService.getAllRevisions( noteIdOrAlias, ); + return Promise.all( + revisions.map((revision) => + this.revisionsService.toRevisionMetadataDto(revision), + ), + ); } catch (e) { if (e instanceof NotInDBError) { throw new NotFoundException(e.message); @@ -191,12 +218,11 @@ export class NotesController { @Request() req, @Param('noteIdOrAlias') noteIdOrAlias: string, @Param('revisionId') revisionId: number, - ) { + ): Promise { // ToDo: check if user is allowed to view this notes revision try { - return await this.revisionsService.getNoteRevision( - noteIdOrAlias, - revisionId, + return this.revisionsService.toRevisionDto( + await this.revisionsService.getRevision(noteIdOrAlias, revisionId), ); } catch (e) { if (e instanceof NotInDBError) { diff --git a/src/auth/mock-auth.guard.ts b/src/auth/mock-auth.guard.ts index 9d15bc83e..d4f1236b0 100644 --- a/src/auth/mock-auth.guard.ts +++ b/src/auth/mock-auth.guard.ts @@ -5,14 +5,20 @@ */ import { ExecutionContext, Injectable } from '@nestjs/common'; +import { UsersService } from '../users/users.service'; +import { User } from '../users/user.entity'; @Injectable() export class MockAuthGuard { - canActivate(context: ExecutionContext) { + private user: User; + constructor(private usersService: UsersService) {} + + async canActivate(context: ExecutionContext) { const req = context.switchToHttp().getRequest(); - req.user = { - userName: 'hardcoded', - }; + if (!this.user) { + this.user = await this.usersService.createUser('hardcoded', 'Testy'); + } + req.user = this.user; return true; } } diff --git a/src/media/media-upload-url.dto.ts b/src/media/media-upload-url.dto.ts new file mode 100644 index 000000000..9e9c9296c --- /dev/null +++ b/src/media/media-upload-url.dto.ts @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { IsString } from 'class-validator'; + +export class MediaUploadUrlDto { + @IsString() + link: string; +} diff --git a/src/media/media.service.ts b/src/media/media.service.ts index 8b5dfdecb..b22aa8aab 100644 --- a/src/media/media.service.ts +++ b/src/media/media.service.ts @@ -18,6 +18,7 @@ import { BackendType } from './backends/backend-type.enum'; import { FilesystemBackend } from './backends/filesystem-backend'; import { MediaBackend } from './media-backend.interface'; import { MediaUpload } from './media-upload.entity'; +import { MediaUploadUrlDto } from './media-upload-url.dto'; @Injectable() export class MediaService { @@ -58,7 +59,11 @@ export class MediaService { return allowedTypes.includes(mimeType); } - public async saveFile(fileBuffer: Buffer, username: string, noteId: string) { + public async saveFile( + fileBuffer: Buffer, + username: string, + noteId: string, + ): Promise { this.logger.debug( `Saving file for note '${noteId}' and user '${username}'`, 'saveFile', @@ -88,7 +93,7 @@ export class MediaService { return url; } - public async deleteFile(filename: string, username: string) { + public async deleteFile(filename: string, username: string): Promise { this.logger.debug( `Deleting '${filename}' for user '${username}'`, 'deleteFile', @@ -132,4 +137,10 @@ export class MediaService { return this.moduleRef.get(FilesystemBackend); } } + + toMediaUploadUrlDto(url: string): MediaUploadUrlDto { + return { + link: url, + }; + } } diff --git a/src/notes/notes.service.ts b/src/notes/notes.service.ts index 8e1f11284..0e3998987 100644 --- a/src/notes/notes.service.ts +++ b/src/notes/notes.service.ts @@ -35,48 +35,26 @@ export class NotesService { this.logger.setContext(NotesService.name); } - getUserNotes(username: string): NoteMetadataDto[] { + getUserNotes(user: User): Note[] { this.logger.warn('Using hardcoded data!'); return [ { - alias: null, - createTime: new Date(), - description: 'Very descriptive text.', - editedBy: [], id: 'foobar-barfoo', - permissions: { - owner: { - displayName: 'foo', - userName: 'fooUser', - email: 'foo@example.com', - photo: '', - }, - sharedToUsers: [], - sharedToGroups: [], - }, + alias: null, + shortid: 'abc', + owner: user, + description: 'Very descriptive text.', + userPermissions: [], + groupPermissions: [], tags: [], + revisions: Promise.resolve([]), + authorColors: [], title: 'Title!', - updateTime: new Date(), - updateUser: { - displayName: 'foo', - userName: 'fooUser', - email: 'foo@example.com', - photo: '', - }, - viewCount: 42, + viewcount: 42, }, ]; } - async createNoteDto( - noteContent: string, - alias?: NoteMetadataDto['alias'], - owner?: User, - ): Promise { - const note = await this.createNote(noteContent, alias, owner); - return this.toNoteDto(note); - } - async createNote( noteContent: string, alias?: NoteMetadataDto['alias'], @@ -96,7 +74,7 @@ export class NotesService { return this.noteRepository.save(newNote); } - async getCurrentContent(note: Note) { + async getCurrentContent(note: Note): Promise { return (await this.getLatestRevision(note)).content; } @@ -108,42 +86,6 @@ export class NotesService { return this.revisionsService.getFirstRevision(note.id); } - async getMetadata(note: Note): Promise { - return { - // TODO: Convert DB UUID to base64 - id: note.id, - alias: note.alias, - title: note.title, - createTime: (await this.getFirstRevision(note)).createdAt, - description: note.description, - editedBy: note.authorColors.map( - (authorColor) => authorColor.user.userName, - ), - // TODO: Extract into method - permissions: { - owner: this.usersService.toUserDto(note.owner), - sharedToUsers: note.userPermissions.map((noteUserPermission) => ({ - user: this.usersService.toUserDto(noteUserPermission.user), - canEdit: noteUserPermission.canEdit, - })), - sharedToGroups: note.groupPermissions.map((noteGroupPermission) => ({ - group: noteGroupPermission.group, - canEdit: noteGroupPermission.canEdit, - })), - }, - tags: note.tags.map((tag) => tag.name), - updateTime: (await this.getLatestRevision(note)).createdAt, - // TODO: Get actual updateUser - updateUser: { - displayName: 'Hardcoded User', - userName: 'hardcoded', - email: 'foo@example.com', - photo: '', - }, - viewCount: 42, - }; - } - async getNoteByIdOrAlias(noteIdOrAlias: string): Promise { this.logger.debug( `Trying to find note '${noteIdOrAlias}'`, @@ -178,45 +120,50 @@ export class NotesService { return note; } - async getNoteDtoByIdOrAlias(noteIdOrAlias: string): Promise { - const note = await this.getNoteByIdOrAlias(noteIdOrAlias); - return this.toNoteDto(note); - } - async deleteNoteByIdOrAlias(noteIdOrAlias: string) { const note = await this.getNoteByIdOrAlias(noteIdOrAlias); return await this.noteRepository.remove(note); } - async updateNoteByIdOrAlias(noteIdOrAlias: string, noteContent: string) { + async updateNoteByIdOrAlias( + noteIdOrAlias: string, + noteContent: string, + ): Promise { const note = await this.getNoteByIdOrAlias(noteIdOrAlias); const revisions = await note.revisions; //TODO: Calculate patch revisions.push(Revision.create(noteContent, noteContent)); note.revisions = Promise.resolve(revisions); - await this.noteRepository.save(note); - return this.toNoteDto(note); - } - - async getNoteMetadata(noteIdOrAlias: string): Promise { - const note = await this.getNoteByIdOrAlias(noteIdOrAlias); - return this.getMetadata(note); + return this.noteRepository.save(note); } updateNotePermissions( noteIdOrAlias: string, newPermissions: NotePermissionsUpdateDto, - ): NotePermissionsDto { + ): Note { this.logger.warn('Using hardcoded data!'); return { + id: 'foobar-barfoo', + alias: null, + shortid: 'abc', owner: { - displayName: 'foo', - userName: 'fooUser', - email: 'foo@example.com', - photo: '', + authTokens: [], + createdAt: new Date(), + displayName: 'hardcoded', + id: '1', + identities: [], + ownedNotes: [], + updatedAt: new Date(), + userName: 'Testy', }, - sharedToUsers: [], - sharedToGroups: [], + description: 'Very descriptive text.', + userPermissions: [], + groupPermissions: [], + tags: [], + revisions: Promise.resolve([]), + authorColors: [], + title: 'Title!', + viewcount: 42, }; } @@ -225,10 +172,50 @@ export class NotesService { return this.getCurrentContent(note); } + async toNotePermissionsDto(note: Note): Promise { + return { + owner: this.usersService.toUserDto(note.owner), + sharedToUsers: note.userPermissions.map((noteUserPermission) => ({ + user: this.usersService.toUserDto(noteUserPermission.user), + canEdit: noteUserPermission.canEdit, + })), + sharedToGroups: note.groupPermissions.map((noteGroupPermission) => ({ + group: noteGroupPermission.group, + canEdit: noteGroupPermission.canEdit, + })), + }; + } + + async toNoteMetadataDto(note: Note): Promise { + return { + // TODO: Convert DB UUID to base64 + id: note.id, + alias: note.alias, + title: note.title, + createTime: (await this.getFirstRevision(note)).createdAt, + description: note.description, + editedBy: note.authorColors.map( + (authorColor) => authorColor.user.userName, + ), + // TODO: Extract into method + permissions: await this.toNotePermissionsDto(note), + tags: note.tags.map((tag) => tag.name), + updateTime: (await this.getLatestRevision(note)).createdAt, + // TODO: Get actual updateUser + updateUser: { + displayName: 'Hardcoded User', + userName: 'hardcoded', + email: 'foo@example.com', + photo: '', + }, + viewCount: 42, + }; + } + async toNoteDto(note: Note): Promise { return { content: await this.getCurrentContent(note), - metadata: await this.getMetadata(note), + metadata: await this.toNoteMetadataDto(note), editedByAtPosition: [], }; } diff --git a/src/revisions/revisions.service.ts b/src/revisions/revisions.service.ts index 389fe422d..a3735337d 100644 --- a/src/revisions/revisions.service.ts +++ b/src/revisions/revisions.service.ts @@ -24,30 +24,26 @@ export class RevisionsService { this.logger.setContext(RevisionsService.name); } - async getNoteRevisionMetadatas( - noteIdOrAlias: string, - ): Promise { + async getAllRevisions(noteIdOrAlias: string): Promise { const note = await this.notesService.getNoteByIdOrAlias(noteIdOrAlias); - const revisions = await this.revisionRepository.find({ + return await this.revisionRepository.find({ where: { - note: note.id, + note: note, }, }); - return revisions.map((revision) => this.toMetadataDto(revision)); } - async getNoteRevision( + async getRevision( noteIdOrAlias: string, revisionId: number, - ): Promise { + ): Promise { const note = await this.notesService.getNoteByIdOrAlias(noteIdOrAlias); - const revision = await this.revisionRepository.findOne({ + return await this.revisionRepository.findOne({ where: { id: revisionId, note: note, }, }); - return this.toDto(revision); } getLatestRevision(noteId: string): Promise { @@ -73,7 +69,7 @@ export class RevisionsService { }); } - toMetadataDto(revision: Revision): RevisionMetadataDto { + toRevisionMetadataDto(revision: Revision): RevisionMetadataDto { return { id: revision.id, length: revision.length, @@ -81,7 +77,7 @@ export class RevisionsService { }; } - toDto(revision: Revision): RevisionDto { + toRevisionDto(revision: Revision): RevisionDto { return { id: revision.id, content: revision.content, @@ -90,7 +86,7 @@ export class RevisionsService { }; } - createRevision(content: string) { + createRevision(content: string): Revision { // TODO: Add previous revision // TODO: Calculate patch // TODO: Save metadata diff --git a/test/public-api/media.e2e-spec.ts b/test/public-api/media.e2e-spec.ts index f4b57ebf0..26ee76d27 100644 --- a/test/public-api/media.e2e-spec.ts +++ b/test/public-api/media.e2e-spec.ts @@ -20,7 +20,6 @@ import { MediaService } from '../../src/media/media.service'; import { NotesModule } from '../../src/notes/notes.module'; import { NotesService } from '../../src/notes/notes.service'; import { PermissionsModule } from '../../src/permissions/permissions.module'; -import { UsersService } from '../../src/users/users.service'; import { AuthModule } from '../../src/auth/auth.module'; import { TokenAuthGuard } from '../../src/auth/token-auth.guard'; import { MockAuthGuard } from '../../src/auth/mock-auth.guard'; @@ -65,8 +64,6 @@ describe('Notes', () => { app.useLogger(logger); const notesService: NotesService = moduleRef.get('NotesService'); await notesService.createNote('test content', 'test_upload_media'); - const usersService: UsersService = moduleRef.get('UsersService'); - await usersService.createUser('hardcoded', 'Hard Coded'); mediaService = moduleRef.get('MediaService'); }); diff --git a/test/public-api/notes.e2e-spec.ts b/test/public-api/notes.e2e-spec.ts index bf9ca8c55..5059f3e93 100644 --- a/test/public-api/notes.e2e-spec.ts +++ b/test/public-api/notes.e2e-spec.ts @@ -20,7 +20,7 @@ import { PermissionsModule } from '../../src/permissions/permissions.module'; import { AuthModule } from '../../src/auth/auth.module'; import { TokenAuthGuard } from '../../src/auth/token-auth.guard'; import { MockAuthGuard } from '../../src/auth/mock-auth.guard'; -import { UsersService } from '../../src/users/users.service'; +import { UsersModule } from '../../src/users/users.module'; describe('Notes', () => { let app: INestApplication; @@ -46,6 +46,7 @@ describe('Notes', () => { }), LoggerModule, AuthModule, + UsersModule, ], }) .overrideGuard(TokenAuthGuard) @@ -55,8 +56,6 @@ describe('Notes', () => { app = moduleRef.createNestApplication(); await app.init(); notesService = moduleRef.get(NotesService); - const usersService: UsersService = moduleRef.get('UsersService'); - await usersService.createUser('testy', 'Testy McTestFace'); }); it(`POST /notes`, async () => { @@ -69,8 +68,9 @@ describe('Notes', () => { .expect(201); expect(response.body.metadata?.id).toBeDefined(); expect( - (await notesService.getNoteDtoByIdOrAlias(response.body.metadata.id)) - .content, + await notesService.getCurrentContent( + await notesService.getNoteByIdOrAlias(response.body.metadata.id), + ), ).toEqual(newNote); }); @@ -100,8 +100,9 @@ describe('Notes', () => { .expect(201); expect(response.body.metadata?.id).toBeDefined(); return expect( - (await notesService.getNoteDtoByIdOrAlias(response.body.metadata.id)) - .content, + await notesService.getCurrentContent( + await notesService.getNoteByIdOrAlias(response.body.metadata?.id), + ), ).toEqual(newNote); }); @@ -125,7 +126,9 @@ describe('Notes', () => { .send('New note text') .expect(200); await expect( - (await notesService.getNoteDtoByIdOrAlias('test4')).content, + await notesService.getCurrentContent( + await notesService.getNoteByIdOrAlias('test4'), + ), ).toEqual('New note text'); expect(response.body.content).toEqual('New note text');