refactor(controller): improve api responses of controller methods

This should drastically improve how accurate our api document is.

Signed-off-by: Philip Molares <philip.molares@udo.edu>
This commit is contained in:
Philip Molares
2026-05-13 22:47:04 +02:00
committed by Erik Michelson
parent c072fd657d
commit 3a4f2c8551
17 changed files with 62 additions and 46 deletions
@@ -30,7 +30,7 @@ import { PermissionLevel } from '@hedgedoc/commons';
import { NoteAliasesDto } from '../../../dtos/note-aliases.dto';
@UseGuards(SessionGuard)
@OpenApi(401)
@OpenApi(401, 403, 429)
@ApiTags('alias')
@Controller('alias')
export class AliasController {
@@ -19,7 +19,7 @@ import { OpenApi } from '../../utils/decorators/openapi.decorator';
import { RequestUserId } from '../../utils/decorators/request-user-id.decorator';
@UseGuards(SessionGuard)
@OpenApi(401)
@OpenApi(401, 403, 429)
@ApiTags('tokens')
@Controller('tokens')
export class ApiTokensController {
@@ -27,7 +27,7 @@ export class GuestController {
@UseGuards(GuestsEnabledGuard)
@Post('register')
@OpenApi(201, 403)
@OpenApi(201, 403, 429)
async registerGuestUser(
@Req() request: RequestWithSession,
): Promise<GuestRegistrationResponseDto> {
@@ -42,7 +42,7 @@ export class GuestController {
@UseGuards(GuestsEnabledGuard)
@Post('login')
@OpenApi(204, 400)
@OpenApi(204, 400, 429)
async loginGuestUser(
@Req() request: RequestWithSession,
@Body() loginDto: GuestLoginDto,
@@ -54,7 +54,7 @@ export class LocalController {
@UseGuards(LoginEnabledGuard, SessionGuard)
@Put()
@OpenApi(200, 400, 401)
@OpenApi(200, 400, 401, 403, 429)
async updatePassword(
@RequestUserId() userId: number,
@Body() changePasswordDto: UpdatePasswordDto,
@@ -70,7 +70,7 @@ export class LocalController {
@UseGuards(LoginEnabledGuard)
@Post('login')
@OpenApi(201, 400, 401)
@OpenApi(201, 400, 401, 429)
async login(
@Req()
request: RequestWithSession,
@@ -44,7 +44,7 @@ export class OidcController {
@Get(':oidcIdentifier')
@Redirect()
@OpenApi(201, 400, 401)
@OpenApi(201, 400, 401, 429)
loginWithOpenIdConnect(
@Req() request: RequestWithSession,
@Param('oidcIdentifier') oidcIdentifier: string,
@@ -30,7 +30,7 @@ import { RequestUserId } from '../../utils/decorators/request-user-id.decorator'
type OptionalNoteType = NoteType | '';
@UseGuards(SessionGuard)
@OpenApi(401, 403)
@OpenApi(401, 403, 429)
@ApiTags('explore')
@Controller('explore')
export class ExploreController {
@@ -13,7 +13,7 @@ import { ConsoleLoggerService } from '../../../logger/console-logger.service';
import { OpenApi } from '../../utils/decorators/openapi.decorator';
@UseGuards(SessionGuard)
@OpenApi(401, 403)
@OpenApi(401, 403, 429)
@ApiTags('groups')
@Controller('groups')
export class GroupsController {
+1 -1
View File
@@ -29,7 +29,7 @@ import { promisify } from 'node:util';
import { RequestWithSession } from '../../utils/request.type';
@UseGuards(SessionGuard)
@OpenApi(401)
@OpenApi(401, 403, 429)
@ApiTags('me')
@Controller('me')
export class MeController {
@@ -33,7 +33,7 @@ import { RequestUserId } from '../../utils/decorators/request-user-id.decorator'
import { NoteHeaderInterceptor } from '../../utils/interceptors/note-header.interceptor';
@UseGuards(SessionGuard)
@OpenApi(401)
@OpenApi(401, 403, 429)
@ApiTags('media')
@Controller('media')
export class MediaController {
@@ -53,7 +53,7 @@ import { RequestUserId } from '../../utils/decorators/request-user-id.decorator'
import { GetNoteIdInterceptor } from '../../utils/interceptors/get-note-id.interceptor';
@UseGuards(SessionGuard, PermissionsGuard)
@OpenApi(401, 403)
@OpenApi(401, 403, 429)
@ApiTags('notes')
@Controller('notes')
export class NotesController {
@@ -181,7 +181,7 @@ export class NotesController {
}
@Put(':noteAlias/metadata/permissions/users/:username')
@OpenApi(200, 403, 404)
@OpenApi(200, 404)
@UseInterceptors(GetNoteIdInterceptor)
@RequirePermission(PermissionLevel.FULL)
async setUserPermission(
@@ -194,9 +194,10 @@ export class NotesController {
return await this.permissionService.getPermissionsDtoForNote(noteId);
}
@Delete(':noteAlias/metadata/permissions/users/:username')
@OpenApi(200, 400)
@UseInterceptors(GetNoteIdInterceptor)
@RequirePermission(PermissionLevel.FULL)
@Delete(':noteAlias/metadata/permissions/users/:username')
async removeUserPermission(
@RequestNoteId() noteId: number,
@Param('username') username: NoteUserPermissionEntryDto['username'],
@@ -213,9 +214,10 @@ export class NotesController {
}
}
@Put(':noteAlias/metadata/permissions/groups/:groupName')
@OpenApi(200, 400, 404)
@UseInterceptors(GetNoteIdInterceptor)
@RequirePermission(PermissionLevel.FULL)
@Put(':noteAlias/metadata/permissions/groups/:groupName')
async setGroupPermission(
@RequestNoteId() noteId: number,
@Param('groupName') groupName: NoteGroupPermissionUpdateDto['groupName'],
@@ -235,10 +237,11 @@ export class NotesController {
return await this.permissionService.getPermissionsDtoForNote(noteId);
}
@Delete(':noteAlias/metadata/permissions/groups/:groupName')
@OpenApi(200, 404)
@UseInterceptors(GetNoteIdInterceptor)
@RequirePermission(PermissionLevel.FULL)
@UseGuards(PermissionsGuard)
@Delete(':noteAlias/metadata/permissions/groups/:groupName')
async removeGroupPermission(
@RequestNoteId() noteId: number,
@Param('groupName') groupName: NoteGroupPermissionEntryDto['groupName'],
@@ -14,6 +14,7 @@ import { UsersService } from '../../../users/users.service';
import { OpenApi } from '../../utils/decorators/openapi.decorator';
@ApiTags('users')
@OpenApi(403, 429)
@Controller('users')
export class UsersController {
constructor(
@@ -29,7 +29,7 @@ import { RequestUserId } from '../../utils/decorators/request-user-id.decorator'
import { ApiTokenGuard } from '../../utils/guards/api-token.guard';
@UseGuards(ApiTokenGuard)
@OpenApi(401)
@OpenApi(401, 403, 429)
@ApiTags('alias')
@ApiSecurity('token')
@Controller('alias')
@@ -50,7 +50,6 @@ export class AliasController {
description: 'The new alias',
schema: AliasSchema,
},
403,
404,
)
async addAlias(
@@ -74,7 +73,6 @@ export class AliasController {
description: 'The updated aliases',
schema: AliasSchema,
},
403,
404,
)
async makeAliasPrimary(
@@ -102,7 +100,6 @@ export class AliasController {
description: 'The aliases was deleted',
},
400,
403,
404,
)
async removeAlias(@RequestUserId() user: number, @Param('alias') alias: string): Promise<void> {
+1 -1
View File
@@ -25,7 +25,7 @@ import { RequestUserId } from '../../utils/decorators/request-user-id.decorator'
import { ApiTokenGuard } from '../../utils/guards/api-token.guard';
@UseGuards(ApiTokenGuard)
@OpenApi(401)
@OpenApi(401, 403, 429)
@ApiTags('me')
@ApiSecurity('token')
@Controller('me')
@@ -33,7 +33,7 @@ import { ApiTokenGuard } from '../../utils/guards/api-token.guard';
import { NoteHeaderInterceptor } from '../../utils/interceptors/note-header.interceptor';
@UseGuards(ApiTokenGuard)
@OpenApi(401)
@OpenApi(401, 403, 429)
@ApiTags('media')
@ApiSecurity('token')
@Controller('media')
@@ -70,7 +70,6 @@ export class MediaController {
schema: MediaUploadSchema,
},
400,
403,
404,
500,
)
@@ -105,7 +104,7 @@ export class MediaController {
}
@Delete(':uuid')
@OpenApi(204, 403, 404, 500)
@OpenApi(204, 404, 500)
async deleteMedia(@RequestUserId() userId: number, @Param('uuid') uuid: string): Promise<void> {
const mediaUpload = await this.mediaService.findUploadByUuid(uuid);
if (await this.permissionsService.checkMediaDeletePermission(userId, uuid)) {
@@ -49,7 +49,7 @@ import { ApiTokenGuard } from '../../utils/guards/api-token.guard';
import { GetNoteIdInterceptor } from '../../utils/interceptors/get-note-id.interceptor';
@UseGuards(ApiTokenGuard, PermissionsGuard)
@OpenApi(401)
@OpenApi(401, 403, 429)
@ApiTags('notes')
@ApiSecurity('token')
@Controller('notes')
@@ -68,7 +68,7 @@ export class NotesController {
@RequirePermission(PermissionLevel.FULL)
@Post()
@OpenApi(201, 403, 409, 413)
@OpenApi(201, 409, 413)
async createNote(
@RequestUserId() userId: number,
@MarkdownBody() text: string,
@@ -86,7 +86,6 @@ export class NotesController {
description: 'Get information about the newly created note',
schema: NoteSchema,
},
403,
404,
)
async getNote(
@@ -105,7 +104,6 @@ export class NotesController {
schema: NoteSchema,
},
400,
403,
409,
413,
)
@@ -122,7 +120,7 @@ export class NotesController {
@UseInterceptors(GetNoteIdInterceptor)
@RequirePermission(PermissionLevel.FULL)
@Delete(':noteAlias')
@OpenApi(204, 403, 404, 500)
@OpenApi(204, 404, 500)
async deleteNote(
@RequestUserId() userId: number,
@RequestNoteId() noteId: number,
@@ -151,7 +149,6 @@ export class NotesController {
description: 'The new, changed note',
schema: NoteSchema,
},
403,
404,
)
async updateNote(
@@ -173,7 +170,6 @@ export class NotesController {
description: 'The raw markdown content of the note',
mimeType: 'text/markdown',
},
403,
404,
)
async getNoteContent(
@@ -192,7 +188,6 @@ export class NotesController {
description: 'The metadata of the note',
schema: NoteMetadataSchema,
},
403,
404,
)
async getNoteMetadata(@RequestNoteId() noteId: number): Promise<NoteMetadataDto> {
@@ -208,7 +203,6 @@ export class NotesController {
description: 'Get the permissions for a note',
schema: NotePermissionsSchema,
},
403,
404,
)
async getPermissions(@RequestNoteId() noteId: number): Promise<NotePermissionsDto> {
@@ -224,7 +218,6 @@ export class NotesController {
description: 'Set the permissions for a user on a note',
schema: NotePermissionsSchema,
},
403,
404,
)
async setUserPermission(
@@ -247,7 +240,6 @@ export class NotesController {
description: 'Remove the permission for a user on a note',
schema: NotePermissionsSchema,
},
403,
404,
)
async removeUserPermission(
@@ -269,7 +261,6 @@ export class NotesController {
description: 'Set the permissions for a group on a note',
schema: NotePermissionsSchema,
},
403,
404,
)
async setGroupPermission(
@@ -292,7 +283,6 @@ export class NotesController {
description: 'Remove the permission for a group on a note',
schema: NotePermissionsSchema,
},
403,
404,
)
async removeGroupPermission(
@@ -313,7 +303,6 @@ export class NotesController {
description: 'Changes the owner of the note',
schema: NoteSchema,
},
403,
404,
)
async changeOwner(
@@ -335,7 +324,6 @@ export class NotesController {
description: 'Changes the owner of the note',
schema: NoteSchema,
},
403,
404,
)
async change(
@@ -357,7 +345,6 @@ export class NotesController {
isArray: true,
schema: RevisionMetadataSchema,
},
403,
404,
)
async getNoteRevisions(@RequestNoteId() noteId: number): Promise<RevisionMetadataDto[]> {
@@ -373,7 +360,6 @@ export class NotesController {
description: 'Revision of the note for the given id or aliases',
schema: RevisionSchema,
},
403,
404,
)
async getNoteRevision(@Param('revisionUuid') revisionUuid: string): Promise<RevisionDto> {
@@ -4,36 +4,53 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { applyDecorators, Header, HttpCode } from '@nestjs/common';
import type { ApiResponseNoStatusOptions } from '@nestjs/swagger';
import {
ApiBadRequestResponse,
ApiConflictResponse,
ApiCreatedResponse,
ApiForbiddenResponse,
ApiFoundResponse,
ApiInternalServerErrorResponse,
ApiNoContentResponse,
ApiNotFoundResponse,
ApiOkResponse,
ApiProduces,
ApiResponseNoStatusOptions,
ApiTooManyRequestsResponse,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';
import { zodToOpenAPI } from 'nestjs-zod';
import { ZodSchema } from 'zod';
import type { ZodSchema } from 'zod';
import {
badRequestDescription,
conflictDescription,
createdDescription,
forbiddenDescription,
foundDescription,
internalServerErrorDescription,
noContentDescription,
notFoundDescription,
okDescription,
payloadTooLargeDescription,
tooManyRequestsDescription,
unauthorizedDescription,
} from '../descriptions';
export type HttpStatusCodes = 200 | 201 | 204 | 302 | 400 | 401 | 403 | 404 | 409 | 413 | 500 | 503;
export type HttpStatusCodes =
| 200
| 201
| 204
| 302
| 400
| 401
| 403
| 404
| 409
| 413
| 429
| 500
| 503;
/**
* Defines what the open api route should document.
@@ -154,6 +171,13 @@ export const OpenApi = (
}),
);
break;
case 403:
decoratorsToApply.push(
ApiForbiddenResponse({
description: description ?? forbiddenDescription,
}),
);
break;
case 404:
decoratorsToApply.push(
ApiNotFoundResponse({
@@ -175,6 +199,13 @@ export const OpenApi = (
}),
);
break;
case 429:
decoratorsToApply.push(
ApiTooManyRequestsResponse({
description: description ?? tooManyRequestsDescription,
}),
);
break;
case 500:
decoratorsToApply.push(
ApiInternalServerErrorResponse({
+1 -2
View File
@@ -12,10 +12,9 @@ export const badRequestDescription = "The request is malformed and can't be proc
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 successfully deleted';
export const unprocessableEntityDescription = "The request change can't be processed";
export const conflictDescription =
'The request conflicts with the current state of the application';
export const payloadTooLargeDescription =
'The note is longer than the maximal allowed length of a note';
export const tooManyRequestsDescription = 'The request triggered a rate limit.';
export const internalServerErrorDescription = 'The request triggered an internal server error.';