From 3620416ed6788f3a2adc94209858fdc701343d88 Mon Sep 17 00:00:00 2001 From: Philip Molares Date: Fri, 19 Mar 2021 12:08:34 +0100 Subject: [PATCH 1/3] Docs: Add ApiProperty to all Dtos This makes it possible for the autogenerated openapi file to contain all the dtos instead of nothing. Signed-off-by: Philip Molares --- src/groups/group-info.dto.ts | 4 ++++ src/history/history-entry-update.dto.ts | 2 ++ src/history/history-entry.dto.ts | 6 ++++++ src/media/media-upload-url.dto.ts | 2 ++ src/media/media-upload.dto.ts | 5 +++++ src/monitoring/monitoring.service.ts | 6 +++--- src/monitoring/server-status.dto.ts | 27 +++++++++++++++++++++---- src/notes/note-authorship.dto.ts | 6 ++++++ src/notes/note-metadata.dto.ts | 15 ++++++++++++++ src/notes/note-permissions.dto.ts | 14 +++++++++++++ src/notes/note.dto.ts | 4 ++++ src/revisions/revision-metadata.dto.ts | 4 ++++ src/revisions/revision.dto.ts | 5 +++++ src/users/user-info.dto.ts | 2 ++ 14 files changed, 95 insertions(+), 7 deletions(-) diff --git a/src/groups/group-info.dto.ts b/src/groups/group-info.dto.ts index 40ea64cb1..c64af1959 100644 --- a/src/groups/group-info.dto.ts +++ b/src/groups/group-info.dto.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { IsBoolean, IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; export class GroupInfoDto { /** @@ -11,6 +12,7 @@ export class GroupInfoDto { * @example "superheroes" */ @IsString() + @ApiProperty() name: string; /** @@ -18,6 +20,7 @@ export class GroupInfoDto { * @example "Superheroes" */ @IsString() + @ApiProperty() displayName: string; /** @@ -26,5 +29,6 @@ export class GroupInfoDto { * @example false */ @IsBoolean() + @ApiProperty() special: boolean; } diff --git a/src/history/history-entry-update.dto.ts b/src/history/history-entry-update.dto.ts index 3aa8e3735..5dc884061 100644 --- a/src/history/history-entry-update.dto.ts +++ b/src/history/history-entry-update.dto.ts @@ -5,11 +5,13 @@ */ import { IsBoolean } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; export class HistoryEntryUpdateDto { /** * True if the note should be pinned */ @IsBoolean() + @ApiProperty() pinStatus: boolean; } diff --git a/src/history/history-entry.dto.ts b/src/history/history-entry.dto.ts index 2312c5a21..6a136b45c 100644 --- a/src/history/history-entry.dto.ts +++ b/src/history/history-entry.dto.ts @@ -5,12 +5,14 @@ */ import { IsArray, IsBoolean, IsDate, IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; export class HistoryEntryDto { /** * ID or Alias of the note */ @IsString() + @ApiProperty() identifier: string; /** @@ -19,6 +21,7 @@ export class HistoryEntryDto { * @example "Shopping List" */ @IsString() + @ApiProperty() title: string; /** @@ -26,10 +29,12 @@ export class HistoryEntryDto { * @example "2020-12-01 12:23:34" */ @IsDate() + @ApiProperty() lastVisited: Date; @IsArray() @IsString({ each: true }) + @ApiProperty() tags: string[]; /** @@ -37,5 +42,6 @@ export class HistoryEntryDto { * @example false */ @IsBoolean() + @ApiProperty() pinStatus: boolean; } diff --git a/src/media/media-upload-url.dto.ts b/src/media/media-upload-url.dto.ts index 9e9c9296c..013c02598 100644 --- a/src/media/media-upload-url.dto.ts +++ b/src/media/media-upload-url.dto.ts @@ -5,8 +5,10 @@ */ import { IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; export class MediaUploadUrlDto { @IsString() + @ApiProperty() link: string; } diff --git a/src/media/media-upload.dto.ts b/src/media/media-upload.dto.ts index 6adf91836..0ba14cf33 100644 --- a/src/media/media-upload.dto.ts +++ b/src/media/media-upload.dto.ts @@ -5,6 +5,7 @@ */ import { IsDate, IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; export class MediaUploadDto { /** @@ -12,6 +13,7 @@ export class MediaUploadDto { * @example "https://example.com/uploads/testfile123.jpg" */ @IsString() + @ApiProperty() url: string; /** @@ -19,6 +21,7 @@ export class MediaUploadDto { * @example "noteId" TODO how looks a note id? */ @IsString() + @ApiProperty() noteId: string; /** @@ -26,6 +29,7 @@ export class MediaUploadDto { * @example "2020-12-01 12:23:34" */ @IsDate() + @ApiProperty() createdAt: Date; /** @@ -33,5 +37,6 @@ export class MediaUploadDto { * @example "testuser5" */ @IsString() + @ApiProperty() userName: string; } diff --git a/src/monitoring/monitoring.service.ts b/src/monitoring/monitoring.service.ts index 260c57312..a410004a1 100644 --- a/src/monitoring/monitoring.service.ts +++ b/src/monitoring/monitoring.service.ts @@ -38,10 +38,10 @@ async function getServerVersionFromPackageJson(): Promise { export class MonitoringService { async getServerStatus(): Promise { return { - connectionSocketQueueLenght: 0, - destictOnlineUsers: 0, + connectionSocketQueueLength: 0, + distinctOnlineUsers: 0, disconnectSocketQueueLength: 0, - distictOnlineRegisteredUsers: 0, + distinctOnlineRegisteredUsers: 0, isConnectionBusy: false, isDisconnectBusy: false, notesCount: 0, diff --git a/src/monitoring/server-status.dto.ts b/src/monitoring/server-status.dto.ts index 8bea15f1e..5752b8ec7 100644 --- a/src/monitoring/server-status.dto.ts +++ b/src/monitoring/server-status.dto.ts @@ -4,25 +4,44 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -export interface ServerVersion { +import { ApiProperty } from '@nestjs/swagger'; + +export class ServerVersion { + @ApiProperty() major: number; + @ApiProperty() minor: number; + @ApiProperty() patch: number; + @ApiProperty() preRelease?: string; + @ApiProperty() commit?: string; } export class ServerStatusDto { + @ApiProperty() serverVersion: ServerVersion; + @ApiProperty() onlineNotes: number; + @ApiProperty() onlineUsers: number; - destictOnlineUsers: number; + @ApiProperty() + distinctOnlineUsers: number; + @ApiProperty() notesCount: number; + @ApiProperty() registeredUsers: number; + @ApiProperty() onlineRegisteredUsers: number; - distictOnlineRegisteredUsers: number; + @ApiProperty() + distinctOnlineRegisteredUsers: number; + @ApiProperty() isConnectionBusy: boolean; - connectionSocketQueueLenght: number; + @ApiProperty() + connectionSocketQueueLength: number; + @ApiProperty() isDisconnectBusy: boolean; + @ApiProperty() disconnectSocketQueueLength: number; } diff --git a/src/notes/note-authorship.dto.ts b/src/notes/note-authorship.dto.ts index 2bffad455..16acac592 100644 --- a/src/notes/note-authorship.dto.ts +++ b/src/notes/note-authorship.dto.ts @@ -6,6 +6,7 @@ import { IsDate, IsNumber, IsString, Min } from 'class-validator'; import { UserInfoDto } from '../users/user-info.dto'; +import { ApiProperty } from '@nestjs/swagger'; export class NoteAuthorshipDto { /** @@ -13,6 +14,7 @@ export class NoteAuthorshipDto { * @example "john.smith" */ @IsString() + @ApiProperty() userName: UserInfoDto['userName']; /** @@ -21,6 +23,7 @@ export class NoteAuthorshipDto { */ @IsNumber() @Min(0) + @ApiProperty() startPos: number; /** @@ -30,6 +33,7 @@ export class NoteAuthorshipDto { */ @IsNumber() @Min(0) + @ApiProperty() endPos: number; /** @@ -37,6 +41,7 @@ export class NoteAuthorshipDto { * @example "2020-12-01 12:23:34" */ @IsDate() + @ApiProperty() createdAt: Date; /** @@ -44,5 +49,6 @@ export class NoteAuthorshipDto { * @example "2020-12-01 12:23:34" */ @IsDate() + @ApiProperty() updatedAt: Date; } diff --git a/src/notes/note-metadata.dto.ts b/src/notes/note-metadata.dto.ts index 638e545a5..03f23011e 100644 --- a/src/notes/note-metadata.dto.ts +++ b/src/notes/note-metadata.dto.ts @@ -14,12 +14,14 @@ import { } from 'class-validator'; import { UserInfoDto } from '../users/user-info.dto'; import { NotePermissionsDto } from './note-permissions.dto'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class NoteMetadataDto { /** * ID of the note */ @IsString() + @ApiProperty() id: string; /** @@ -28,6 +30,7 @@ export class NoteMetadataDto { */ @IsString() @IsOptional() + @ApiPropertyOptional() alias: string; /** @@ -36,6 +39,7 @@ export class NoteMetadataDto { * @example "Shopping List" */ @IsString() + @ApiProperty() title: string; /** @@ -44,6 +48,7 @@ export class NoteMetadataDto { * @example Everything I want to buy */ @IsString() + @ApiProperty() description: string; /** @@ -52,6 +57,7 @@ export class NoteMetadataDto { */ @IsArray() @IsString({ each: true }) + @ApiProperty() tags: string[]; /** @@ -59,12 +65,14 @@ export class NoteMetadataDto { * @example "2020-12-01 12:23:34" */ @IsDate() + @ApiProperty() updateTime: Date; /** * User that last edited the note */ @ValidateNested() + @ApiProperty({ type: UserInfoDto }) updateUser: UserInfoDto; /** @@ -72,6 +80,7 @@ export class NoteMetadataDto { * @example 42 */ @IsNumber() + @ApiProperty() viewCount: number; /** @@ -79,6 +88,7 @@ export class NoteMetadataDto { * @example "2020-12-01 12:23:34" */ @IsDate() + @ApiProperty() createTime: Date; /** @@ -87,12 +97,14 @@ export class NoteMetadataDto { */ @IsArray() @ValidateNested() + @ApiProperty() editedBy: UserInfoDto['userName'][]; /** * Permissions currently in effect for the note */ @ValidateNested() + @ApiProperty({ type: NotePermissionsDto }) permissions: NotePermissionsDto; } @@ -103,6 +115,7 @@ export class NoteMetadataUpdateDto { * @example "Shopping List" */ @IsString() + @ApiProperty() title: string; /** @@ -111,6 +124,7 @@ export class NoteMetadataUpdateDto { * @example Everything I want to buy */ @IsString() + @ApiProperty() description: string; /** @@ -119,5 +133,6 @@ export class NoteMetadataUpdateDto { */ @IsArray() @IsString({ each: true }) + @ApiProperty() tags: string[]; } diff --git a/src/notes/note-permissions.dto.ts b/src/notes/note-permissions.dto.ts index 42da78129..11c7d3c62 100644 --- a/src/notes/note-permissions.dto.ts +++ b/src/notes/note-permissions.dto.ts @@ -7,12 +7,14 @@ import { IsArray, IsBoolean, IsString, ValidateNested } from 'class-validator'; import { UserInfoDto } from '../users/user-info.dto'; import { GroupInfoDto } from '../groups/group-info.dto'; +import { ApiProperty } from '@nestjs/swagger'; export class NoteUserPermissionEntryDto { /** * User this permission applies to */ @ValidateNested() + @ApiProperty({ type: UserInfoDto }) user: UserInfoDto; /** @@ -20,6 +22,7 @@ export class NoteUserPermissionEntryDto { * @example false */ @IsBoolean() + @ApiProperty() canEdit: boolean; } @@ -29,6 +32,7 @@ export class NoteUserPermissionUpdateDto { * @example "john.smith" */ @IsString() + @ApiProperty() username: string; /** @@ -36,6 +40,7 @@ export class NoteUserPermissionUpdateDto { * @example false */ @IsBoolean() + @ApiProperty() canEdit: boolean; } @@ -44,6 +49,7 @@ export class NoteGroupPermissionEntryDto { * Group this permission applies to */ @ValidateNested() + @ApiProperty({ type: GroupInfoDto }) group: GroupInfoDto; /** @@ -51,6 +57,7 @@ export class NoteGroupPermissionEntryDto { * @example false */ @IsBoolean() + @ApiProperty() canEdit: boolean; } @@ -60,6 +67,7 @@ export class NoteGroupPermissionUpdateDto { * @example "superheroes" */ @IsString() + @ApiProperty() groupname: string; /** @@ -67,6 +75,7 @@ export class NoteGroupPermissionUpdateDto { * @example false */ @IsBoolean() + @ApiProperty() canEdit: boolean; } @@ -75,6 +84,7 @@ export class NotePermissionsDto { * User this permission applies to */ @ValidateNested() + @ApiProperty({ type: UserInfoDto }) owner: UserInfoDto; /** @@ -82,6 +92,7 @@ export class NotePermissionsDto { */ @ValidateNested() @IsArray() + @ApiProperty({ isArray: true, type: NoteUserPermissionEntryDto }) sharedToUsers: NoteUserPermissionEntryDto[]; /** @@ -89,6 +100,7 @@ export class NotePermissionsDto { */ @ValidateNested() @IsArray() + @ApiProperty({ isArray: true, type: NoteGroupPermissionEntryDto }) sharedToGroups: NoteGroupPermissionEntryDto[]; } @@ -98,6 +110,7 @@ export class NotePermissionsUpdateDto { */ @IsArray() @ValidateNested() + @ApiProperty({ isArray: true, type: NoteUserPermissionUpdateDto }) sharedToUsers: NoteUserPermissionUpdateDto[]; /** @@ -105,5 +118,6 @@ export class NotePermissionsUpdateDto { */ @IsArray() @ValidateNested() + @ApiProperty({ isArray: true, type: NoteGroupPermissionUpdateDto }) sharedToGroups: NoteGroupPermissionUpdateDto[]; } diff --git a/src/notes/note.dto.ts b/src/notes/note.dto.ts index a32fca9e6..a41b2b9c3 100644 --- a/src/notes/note.dto.ts +++ b/src/notes/note.dto.ts @@ -7,6 +7,7 @@ import { IsArray, IsString, ValidateNested } from 'class-validator'; import { NoteAuthorshipDto } from './note-authorship.dto'; import { NoteMetadataDto } from './note-metadata.dto'; +import { ApiProperty } from '@nestjs/swagger'; export class NoteDto { /** @@ -14,12 +15,14 @@ export class NoteDto { * @example "# I am a heading" */ @IsString() + @ApiProperty() content: string; /** * Metadata of the note */ @ValidateNested() + @ApiProperty({ type: NoteMetadataDto }) metadata: NoteMetadataDto; /** @@ -27,5 +30,6 @@ export class NoteDto { */ @IsArray() @ValidateNested({ each: true }) + @ApiProperty({ isArray: true, type: NoteAuthorshipDto }) editedByAtPosition: NoteAuthorshipDto[]; } diff --git a/src/revisions/revision-metadata.dto.ts b/src/revisions/revision-metadata.dto.ts index a2736d4fe..09f7e6b15 100644 --- a/src/revisions/revision-metadata.dto.ts +++ b/src/revisions/revision-metadata.dto.ts @@ -6,6 +6,7 @@ import { IsDate, IsNumber } from 'class-validator'; import { Revision } from './revision.entity'; +import { ApiProperty } from '@nestjs/swagger'; export class RevisionMetadataDto { /** @@ -13,6 +14,7 @@ export class RevisionMetadataDto { * @example 13 */ @IsNumber() + @ApiProperty() id: Revision['id']; /** @@ -20,6 +22,7 @@ export class RevisionMetadataDto { * @example "2020-12-01 12:23:34" */ @IsDate() + @ApiProperty() createdAt: Date; /** @@ -27,5 +30,6 @@ export class RevisionMetadataDto { * @example 142 */ @IsNumber() + @ApiProperty() length: number; } diff --git a/src/revisions/revision.dto.ts b/src/revisions/revision.dto.ts index 1c9748eca..0bcc350c8 100644 --- a/src/revisions/revision.dto.ts +++ b/src/revisions/revision.dto.ts @@ -6,6 +6,7 @@ import { IsDate, IsNumber, IsString } from 'class-validator'; import { Revision } from './revision.entity'; +import { ApiProperty } from '@nestjs/swagger'; export class RevisionDto { /** @@ -13,6 +14,7 @@ export class RevisionDto { * @example 13 */ @IsNumber() + @ApiProperty() id: Revision['id']; /** @@ -20,12 +22,14 @@ export class RevisionDto { * @example "# I am a heading" */ @IsString() + @ApiProperty() content: string; /** * Patch from the preceding revision to this one */ @IsString() + @ApiProperty() patch: string; /** @@ -33,5 +37,6 @@ export class RevisionDto { * @example "2020-12-01 12:23:34" */ @IsDate() + @ApiProperty() createdAt: Date; } diff --git a/src/users/user-info.dto.ts b/src/users/user-info.dto.ts index d6702b5ac..8de5bf7d1 100644 --- a/src/users/user-info.dto.ts +++ b/src/users/user-info.dto.ts @@ -13,6 +13,7 @@ export class UserInfoDto { * @example "john.smith" */ @IsString() + @ApiProperty() userName: string; /** @@ -20,6 +21,7 @@ export class UserInfoDto { * @example "John Smith" */ @IsString() + @ApiProperty() displayName: string; /** From cb5c135cb7077b28c82afa65ce8a4bc45315d182 Mon Sep 17 00:00:00 2001 From: Philip Molares Date: Fri, 19 Mar 2021 12:09:50 +0100 Subject: [PATCH 2/3] Docs: Add description for common http codes These are the descriptions for all 401, 403, 404 and 204 HTTP responses in HedgeDoc. Signed-off-by: Philip Molares --- src/api/utils/descriptions.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/api/utils/descriptions.ts diff --git a/src/api/utils/descriptions.ts b/src/api/utils/descriptions.ts new file mode 100644 index 000000000..ba1d637fe --- /dev/null +++ b/src/api/utils/descriptions.ts @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const unauthorizedDescription = + 'Authorization information is missing or invalid'; +export const forbiddenDescription = + 'Access to the requested resource is not permitted'; +export const notFoundDescription = 'The requested resource was not found'; +export const successfullyDeletedDescription = + 'The requested resource was sucessfully deleted'; From 168d85778ccd581f23b263dc41a2bbb2a58fa55d Mon Sep 17 00:00:00 2001 From: Philip Molares Date: Fri, 19 Mar 2021 12:10:46 +0100 Subject: [PATCH 3/3] Docs: Add more documentation to the public api Signed-off-by: Philip Molares --- src/api/public/me/me.controller.ts | 53 +++++++++++- src/api/public/media/media.controller.ts | 40 ++++++++- .../monitoring/monitoring.controller.ts | 25 +++++- src/api/public/notes/notes.controller.ts | 84 ++++++++++++++++++- test/public-api/media.e2e-spec.ts | 2 +- test/public-api/notes.e2e-spec.ts | 2 +- 6 files changed, 199 insertions(+), 7 deletions(-) diff --git a/src/api/public/me/me.controller.ts b/src/api/public/me/me.controller.ts index c42d9a4fa..3bc2ae167 100644 --- a/src/api/public/me/me.controller.ts +++ b/src/api/public/me/me.controller.ts @@ -23,14 +23,25 @@ import { NoteMetadataDto } from '../../../notes/note-metadata.dto'; import { NotesService } from '../../../notes/notes.service'; import { UsersService } from '../../../users/users.service'; import { TokenAuthGuard } from '../../../auth/token-auth.guard'; -import { ApiSecurity, ApiTags } from '@nestjs/swagger'; +import { + ApiNoContentResponse, + ApiNotFoundResponse, + ApiOkResponse, + ApiSecurity, + ApiTags, + ApiUnauthorizedResponse, +} from '@nestjs/swagger'; 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'; +import { + notFoundDescription, + successfullyDeletedDescription, + unauthorizedDescription, +} from '../../utils/descriptions'; @ApiTags('me') @ApiSecurity('token') @@ -48,6 +59,11 @@ export class MeController { @UseGuards(TokenAuthGuard) @Get() + @ApiOkResponse({ + description: 'The user information', + type: UserInfoDto, + }) + @ApiUnauthorizedResponse({ description: unauthorizedDescription }) async getMe(@Req() req: Request): Promise { return this.usersService.toUserDto( await this.usersService.getUserByUsername(req.user.userName), @@ -56,6 +72,12 @@ export class MeController { @UseGuards(TokenAuthGuard) @Get('history') + @ApiOkResponse({ + description: 'The history entries of the user', + isArray: true, + type: HistoryEntryDto, + }) + @ApiUnauthorizedResponse({ description: unauthorizedDescription }) async getUserHistory(@Req() req: Request): Promise { const foundEntries = await this.historyService.getEntriesByUser(req.user); return await Promise.all( @@ -65,6 +87,12 @@ export class MeController { @UseGuards(TokenAuthGuard) @Get('history/:note') + @ApiOkResponse({ + description: 'The history entry of the user which points to the note', + type: HistoryEntryDto, + }) + @ApiUnauthorizedResponse({ description: unauthorizedDescription }) + @ApiNotFoundResponse({ description: notFoundDescription }) async getHistoryEntry( @Req() req: Request, @Param('note') note: string, @@ -85,6 +113,12 @@ export class MeController { @UseGuards(TokenAuthGuard) @Put('history/:note') + @ApiOkResponse({ + description: 'The updated history entry', + type: HistoryEntryDto, + }) + @ApiUnauthorizedResponse({ description: unauthorizedDescription }) + @ApiNotFoundResponse({ description: notFoundDescription }) async updateHistoryEntry( @Req() req: Request, @Param('note') note: string, @@ -110,6 +144,9 @@ export class MeController { @UseGuards(TokenAuthGuard) @Delete('history/:note') @HttpCode(204) + @ApiNoContentResponse({ description: successfullyDeletedDescription }) + @ApiUnauthorizedResponse({ description: unauthorizedDescription }) + @ApiNotFoundResponse({ description: notFoundDescription }) async deleteHistoryEntry( @Req() req: Request, @Param('note') note: string, @@ -127,6 +164,12 @@ export class MeController { @UseGuards(TokenAuthGuard) @Get('notes') + @ApiOkResponse({ + description: 'Metadata of all notes of the user', + isArray: true, + type: NoteMetadataDto, + }) + @ApiUnauthorizedResponse({ description: unauthorizedDescription }) async getMyNotes(@Req() req: Request): Promise { const notes = this.notesService.getUserNotes(req.user); return await Promise.all( @@ -136,6 +179,12 @@ export class MeController { @UseGuards(TokenAuthGuard) @Get('media') + @ApiOkResponse({ + description: 'All media uploads of the user', + isArray: true, + type: MediaUploadDto, + }) + @ApiUnauthorizedResponse({ description: unauthorizedDescription }) 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/api/public/media/media.controller.ts b/src/api/public/media/media.controller.ts index 28fff33c3..388121045 100644 --- a/src/api/public/media/media.controller.ts +++ b/src/api/public/media/media.controller.ts @@ -9,6 +9,7 @@ import { Controller, Delete, Headers, + HttpCode, InternalServerErrorException, NotFoundException, Param, @@ -31,8 +32,25 @@ import { ConsoleLoggerService } from '../../../logger/console-logger.service'; import { MediaService } from '../../../media/media.service'; import { MulterFile } from '../../../media/multer-file.interface'; import { TokenAuthGuard } from '../../../auth/token-auth.guard'; -import { ApiSecurity, ApiTags } from '@nestjs/swagger'; +import { + ApiBody, + ApiConsumes, + ApiCreatedResponse, + ApiForbiddenResponse, + ApiHeader, + ApiNoContentResponse, + ApiNotFoundResponse, + ApiSecurity, + ApiTags, + ApiUnauthorizedResponse, +} from '@nestjs/swagger'; import { MediaUploadUrlDto } from '../../../media/media-upload-url.dto'; +import { + forbiddenDescription, + notFoundDescription, + successfullyDeletedDescription, + unauthorizedDescription, +} from '../../utils/descriptions'; @ApiTags('media') @ApiSecurity('token') @@ -47,7 +65,22 @@ export class MediaController { @UseGuards(TokenAuthGuard) @Post() + @ApiConsumes('multipart/form-data') + @ApiBody({ + description: 'The binary file to upload', + }) + @ApiHeader({ + name: 'HedgeDoc-Note', + description: 'ID or alias of the parent note', + }) + @ApiCreatedResponse({ + description: 'The file was uploaded successfully', + type: MediaUploadUrlDto, + }) + @ApiUnauthorizedResponse({ description: unauthorizedDescription }) + @ApiForbiddenResponse({ description: forbiddenDescription }) @UseInterceptors(FileInterceptor('file')) + @HttpCode(201) async uploadMedia( @Req() req: Request, @UploadedFile() file: MulterFile, @@ -80,6 +113,11 @@ export class MediaController { @UseGuards(TokenAuthGuard) @Delete(':filename') + @HttpCode(204) + @ApiNoContentResponse({ description: successfullyDeletedDescription }) + @ApiUnauthorizedResponse({ description: unauthorizedDescription }) + @ApiForbiddenResponse({ description: forbiddenDescription }) + @ApiNotFoundResponse({ description: notFoundDescription }) async deleteMedia( @Req() req: Request, @Param('filename') filename: string, diff --git a/src/api/public/monitoring/monitoring.controller.ts b/src/api/public/monitoring/monitoring.controller.ts index 1905adb08..6f93e04ad 100644 --- a/src/api/public/monitoring/monitoring.controller.ts +++ b/src/api/public/monitoring/monitoring.controller.ts @@ -7,8 +7,19 @@ import { Controller, Get, UseGuards } from '@nestjs/common'; import { MonitoringService } from '../../../monitoring/monitoring.service'; import { TokenAuthGuard } from '../../../auth/token-auth.guard'; -import { ApiSecurity, ApiTags } from '@nestjs/swagger'; +import { + ApiForbiddenResponse, + ApiOkResponse, + ApiProduces, + ApiSecurity, + ApiTags, + ApiUnauthorizedResponse, +} from '@nestjs/swagger'; import { ServerStatusDto } from '../../../monitoring/server-status.dto'; +import { + forbiddenDescription, + unauthorizedDescription, +} from '../../utils/descriptions'; @ApiTags('monitoring') @ApiSecurity('token') @@ -18,6 +29,12 @@ export class MonitoringController { @UseGuards(TokenAuthGuard) @Get() + @ApiOkResponse({ + description: 'The server info', + type: ServerStatusDto, + }) + @ApiUnauthorizedResponse({ description: unauthorizedDescription }) + @ApiForbiddenResponse({ description: forbiddenDescription }) getStatus(): Promise { // TODO: toServerStatusDto. return this.monitoringService.getServerStatus(); @@ -25,6 +42,12 @@ export class MonitoringController { @UseGuards(TokenAuthGuard) @Get('prometheus') + @ApiOkResponse({ + description: 'Prometheus compatible monitoring data', + }) + @ApiUnauthorizedResponse({ description: unauthorizedDescription }) + @ApiForbiddenResponse({ description: forbiddenDescription }) + @ApiProduces('text/plain') getPrometheusStatus(): string { return ''; } diff --git a/src/api/public/notes/notes.controller.ts b/src/api/public/notes/notes.controller.ts index bfc398908..2185284d0 100644 --- a/src/api/public/notes/notes.controller.ts +++ b/src/api/public/notes/notes.controller.ts @@ -11,6 +11,7 @@ import { Delete, Get, Header, + HttpCode, NotFoundException, Param, Post, @@ -34,7 +35,17 @@ 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, ApiTags } from '@nestjs/swagger'; +import { + ApiCreatedResponse, + ApiForbiddenResponse, + ApiNoContentResponse, + ApiNotFoundResponse, + ApiOkResponse, + ApiProduces, + ApiSecurity, + ApiTags, + ApiUnauthorizedResponse, +} from '@nestjs/swagger'; import { HistoryService } from '../../../history/history.service'; import { NoteDto } from '../../../notes/note.dto'; import { NoteMetadataDto } from '../../../notes/note-metadata.dto'; @@ -43,6 +54,12 @@ import { RevisionDto } from '../../../revisions/revision.dto'; import { PermissionsService } from '../../../permissions/permissions.service'; import { Note } from '../../../notes/note.entity'; import { Request } from 'express'; +import { + forbiddenDescription, + notFoundDescription, + successfullyDeletedDescription, + unauthorizedDescription, +} from '../../utils/descriptions'; @ApiTags('notes') @ApiSecurity('token') @@ -60,6 +77,9 @@ export class NotesController { @UseGuards(TokenAuthGuard) @Post() + @HttpCode(201) + @ApiUnauthorizedResponse({ description: unauthorizedDescription }) + @ApiForbiddenResponse({ description: forbiddenDescription }) async createNote( @Req() req: Request, @MarkdownBody() text: string, @@ -76,6 +96,13 @@ export class NotesController { @UseGuards(TokenAuthGuard) @Get(':noteIdOrAlias') + @ApiOkResponse({ + description: 'Get information about the newly created note', + type: NoteDto, + }) + @ApiUnauthorizedResponse({ description: unauthorizedDescription }) + @ApiForbiddenResponse({ description: forbiddenDescription }) + @ApiNotFoundResponse({ description: notFoundDescription }) async getNote( @Req() req: Request, @Param('noteIdOrAlias') noteIdOrAlias: string, @@ -101,6 +128,13 @@ export class NotesController { @UseGuards(TokenAuthGuard) @Post(':noteAlias') + @HttpCode(201) + @ApiCreatedResponse({ + description: 'Get information about the newly created note', + type: NoteDto, + }) + @ApiUnauthorizedResponse({ description: unauthorizedDescription }) + @ApiForbiddenResponse({ description: forbiddenDescription }) async createNamedNote( @Req() req: Request, @Param('noteAlias') noteAlias: string, @@ -127,6 +161,11 @@ export class NotesController { @UseGuards(TokenAuthGuard) @Delete(':noteIdOrAlias') + @HttpCode(204) + @ApiNoContentResponse({ description: successfullyDeletedDescription }) + @ApiUnauthorizedResponse({ description: unauthorizedDescription }) + @ApiForbiddenResponse({ description: forbiddenDescription }) + @ApiNotFoundResponse({ description: notFoundDescription }) async deleteNote( @Req() req: Request, @Param('noteIdOrAlias') noteIdOrAlias: string, @@ -153,6 +192,13 @@ export class NotesController { @UseGuards(TokenAuthGuard) @Put(':noteIdOrAlias') + @ApiOkResponse({ + description: 'The new, changed note', + type: NoteDto, + }) + @ApiUnauthorizedResponse({ description: unauthorizedDescription }) + @ApiForbiddenResponse({ description: forbiddenDescription }) + @ApiNotFoundResponse({ description: notFoundDescription }) async updateNote( @Req() req: Request, @Param('noteIdOrAlias') noteIdOrAlias: string, @@ -180,6 +226,13 @@ export class NotesController { @UseGuards(TokenAuthGuard) @Get(':noteIdOrAlias/content') + @ApiProduces('text/markdown') + @ApiOkResponse({ + description: 'The raw markdown content of the note', + }) + @ApiUnauthorizedResponse({ description: unauthorizedDescription }) + @ApiForbiddenResponse({ description: forbiddenDescription }) + @ApiNotFoundResponse({ description: notFoundDescription }) @Header('content-type', 'text/markdown') async getNoteContent( @Req() req: Request, @@ -204,6 +257,13 @@ export class NotesController { @UseGuards(TokenAuthGuard) @Get(':noteIdOrAlias/metadata') + @ApiOkResponse({ + description: 'The metadata of the note', + type: NoteMetadataDto, + }) + @ApiUnauthorizedResponse({ description: unauthorizedDescription }) + @ApiForbiddenResponse({ description: forbiddenDescription }) + @ApiNotFoundResponse({ description: notFoundDescription }) async getNoteMetadata( @Req() req: Request, @Param('noteIdOrAlias') noteIdOrAlias: string, @@ -230,6 +290,13 @@ export class NotesController { @UseGuards(TokenAuthGuard) @Put(':noteIdOrAlias/metadata/permissions') + @ApiOkResponse({ + description: 'The updated permissions of the note', + type: NotePermissionsDto, + }) + @ApiUnauthorizedResponse({ description: unauthorizedDescription }) + @ApiForbiddenResponse({ description: forbiddenDescription }) + @ApiNotFoundResponse({ description: notFoundDescription }) async updateNotePermissions( @Req() req: Request, @Param('noteIdOrAlias') noteIdOrAlias: string, @@ -256,6 +323,14 @@ export class NotesController { @UseGuards(TokenAuthGuard) @Get(':noteIdOrAlias/revisions') + @ApiOkResponse({ + description: 'Revisions of the note', + isArray: true, + type: RevisionMetadataDto, + }) + @ApiUnauthorizedResponse({ description: unauthorizedDescription }) + @ApiForbiddenResponse({ description: forbiddenDescription }) + @ApiNotFoundResponse({ description: notFoundDescription }) async getNoteRevisions( @Req() req: Request, @Param('noteIdOrAlias') noteIdOrAlias: string, @@ -284,6 +359,13 @@ export class NotesController { @UseGuards(TokenAuthGuard) @Get(':noteIdOrAlias/revisions/:revisionId') + @ApiOkResponse({ + description: 'Revision of the note for the given id or alias', + type: RevisionDto, + }) + @ApiUnauthorizedResponse({ description: unauthorizedDescription }) + @ApiForbiddenResponse({ description: forbiddenDescription }) + @ApiNotFoundResponse({ description: notFoundDescription }) async getNoteRevision( @Req() req: Request, @Param('noteIdOrAlias') noteIdOrAlias: string, diff --git a/test/public-api/media.e2e-spec.ts b/test/public-api/media.e2e-spec.ts index 0c5d67ad1..a3d15d622 100644 --- a/test/public-api/media.e2e-spec.ts +++ b/test/public-api/media.e2e-spec.ts @@ -104,7 +104,7 @@ describe('Notes', () => { const filename = url.split('/').pop(); await request(app.getHttpServer()) .delete('/media/' + filename) - .expect(200); + .expect(204); }); afterAll(async () => { diff --git a/test/public-api/notes.e2e-spec.ts b/test/public-api/notes.e2e-spec.ts index 5a5b5537a..155ca9c86 100644 --- a/test/public-api/notes.e2e-spec.ts +++ b/test/public-api/notes.e2e-spec.ts @@ -146,7 +146,7 @@ describe('Notes', () => { describe('DELETE /notes/{note}', () => { it('works with an existing alias', async () => { await notesService.createNote(content, 'test3', user); - await request(app.getHttpServer()).delete('/notes/test3').expect(200); + await request(app.getHttpServer()).delete('/notes/test3').expect(204); await expect(notesService.getNoteByIdOrAlias('test3')).rejects.toEqual( new NotInDBError("Note with id/alias 'test3' not found."), );