From 72161902f9f7cf5510365b0ac0a57f595919f016 Mon Sep 17 00:00:00 2001 From: Stepan Usatiuk Date: Thu, 4 Jan 2024 17:18:01 +0100 Subject: [PATCH] somewhat documented API --- docker-compose.example.yml | 6 +- .../tjv/y/server/OpenAPIConfiguration.java | 29 ------- .../controller/ApiExceptionHandler.java | 15 ++++ .../y/server/controller/ChatController.java | 33 ++++++++ .../server/controller/MessageController.java | 29 ++++++- .../y/server/controller/PersonController.java | 78 +++++++++++++++---- .../y/server/controller/PostController.java | 33 ++++++++ .../y/server/controller/TokenController.java | 14 ++++ .../annotations/ApiBadRequestResponse.java | 24 ++++++ .../annotations/ApiConflictResponse.java | 24 ++++++ .../annotations/ApiForbiddenResponse.java | 24 ++++++ .../annotations/ApiNotFoundResponse.java | 24 ++++++ .../annotations/ApiUnauthorizedResponse.java | 24 ++++++ .../annotations/ChatToArrResponse.java | 22 ++++++ .../annotations/ChatToResponse.java | 23 ++++++ .../annotations/MessageToArrResponse.java | 22 ++++++ .../annotations/MessageToResponse.java | 22 ++++++ .../annotations/PersonToArrResponse.java | 22 ++++++ .../annotations/PersonToResponse.java | 24 ++++++ .../annotations/PostToArrResponse.java | 22 ++++++ .../annotations/PostToResponse.java | 22 ++++++ .../tjv/y/server/dto/ChatCreateTo.java | 6 +- .../tjv/y/server/dto/MessageCreateTo.java | 4 +- .../tjv/y/server/dto/PersonCreateTo.java | 6 +- .../tjv/y/server/dto/PostCreateTo.java | 4 +- .../tjv/y/server/dto/TokenRequestTo.java | 4 +- .../tjv/y/server/service/ChatService.java | 2 +- .../tjv/y/server/spasupport/WebConfig.java | 1 + 28 files changed, 509 insertions(+), 54 deletions(-) create mode 100644 server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/ApiBadRequestResponse.java create mode 100644 server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/ApiConflictResponse.java create mode 100644 server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/ApiForbiddenResponse.java create mode 100644 server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/ApiNotFoundResponse.java create mode 100644 server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/ApiUnauthorizedResponse.java create mode 100644 server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/ChatToArrResponse.java create mode 100644 server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/ChatToResponse.java create mode 100644 server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/MessageToArrResponse.java create mode 100644 server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/MessageToResponse.java create mode 100644 server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/PersonToArrResponse.java create mode 100644 server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/PersonToResponse.java create mode 100644 server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/PostToArrResponse.java create mode 100644 server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/PostToResponse.java diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 595677b..fd4135c 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -29,11 +29,9 @@ services: - ymariadb:/var/lib/mysql healthcheck: test: [ "CMD", "healthcheck.sh", "--connect", "--innodb_initialized" ] - start_period: 30s - start_interval: 10s interval: 5s timeout: 5s - retries: 3 + retries: 10 volumes: - ymariadb: \ No newline at end of file + ymariadb: diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/OpenAPIConfiguration.java b/server/src/main/java/com/usatiuk/tjv/y/server/OpenAPIConfiguration.java index 9f301ff..7b9b680 100644 --- a/server/src/main/java/com/usatiuk/tjv/y/server/OpenAPIConfiguration.java +++ b/server/src/main/java/com/usatiuk/tjv/y/server/OpenAPIConfiguration.java @@ -1,19 +1,10 @@ package com.usatiuk.tjv.y.server; -import com.usatiuk.tjv.y.server.dto.ErrorTo; -import io.swagger.v3.core.converter.AnnotatedType; -import io.swagger.v3.core.converter.ModelConverters; import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; import io.swagger.v3.oas.annotations.info.Info; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityScheme; -import io.swagger.v3.oas.models.media.Content; -import io.swagger.v3.oas.models.media.MediaType; -import io.swagger.v3.oas.models.media.Schema; -import io.swagger.v3.oas.models.responses.ApiResponse; -import org.springdoc.core.customizers.OpenApiCustomizer; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @@ -25,24 +16,4 @@ import org.springframework.context.annotation.Configuration; scheme = "bearer" ) public class OpenAPIConfiguration { - private ApiResponse createApiResponse(String message, Schema schema) { - MediaType mediaType = new MediaType(); - mediaType.schema(schema); - return new ApiResponse().description(message) - .content(new Content().addMediaType(org.springframework.http.MediaType.APPLICATION_JSON_VALUE, mediaType)); - } - - @Bean - public OpenApiCustomizer customizer() { - return openApi -> { - Schema errorResponseSchema = ModelConverters.getInstance() - .resolveAsResolvedSchema(new AnnotatedType(ErrorTo.class)).schema; - openApi.getPaths().values().forEach(pathItem -> pathItem.readOperations().forEach(operation -> { - var apiResponses = operation.getResponses(); - apiResponses.addApiResponse("500", createApiResponse("Server Error", errorResponseSchema)); - })); - }; - - - } } diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/controller/ApiExceptionHandler.java b/server/src/main/java/com/usatiuk/tjv/y/server/controller/ApiExceptionHandler.java index b6b9fd5..de1c400 100644 --- a/server/src/main/java/com/usatiuk/tjv/y/server/controller/ApiExceptionHandler.java +++ b/server/src/main/java/com/usatiuk/tjv/y/server/controller/ApiExceptionHandler.java @@ -7,9 +7,11 @@ import com.usatiuk.tjv.y.server.service.exceptions.NotFoundException; import jakarta.persistence.EntityNotFoundException; import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.orm.jpa.JpaObjectRetrievalFailureException; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UsernameNotFoundException; @@ -43,6 +45,13 @@ public class ApiExceptionHandler extends ResponseEntityExceptionHandler { } } + @ExceptionHandler(DataIntegrityViolationException.class) + protected ResponseEntity handleDataIntegrityViolationException(DataIntegrityViolationException ex, WebRequest request) { + return handleExceptionInternal(ex, + new ErrorTo(List.of("Something is wrong with your request"), HttpStatus.BAD_REQUEST.value()), + new HttpHeaders(), HttpStatus.BAD_REQUEST, request); + } + @ExceptionHandler(AuthenticationException.class) protected ResponseEntity handleAuthenticationException(AuthenticationException ex, WebRequest request) { return handleExceptionInternal(ex, @@ -92,6 +101,12 @@ public class ApiExceptionHandler extends ResponseEntityExceptionHandler { new HttpHeaders(), HttpStatus.NOT_FOUND, request); } + @ExceptionHandler(JpaObjectRetrievalFailureException.class) + protected ResponseEntity handleEJpaObjectRetrievalFailureException(JpaObjectRetrievalFailureException ex, WebRequest request) { + return handleExceptionInternal(ex, + new ErrorTo(List.of(ex.getMessage()), HttpStatus.NOT_FOUND.value()), + new HttpHeaders(), HttpStatus.NOT_FOUND, request); + } @ExceptionHandler(ResponseStatusException.class) protected ResponseEntity handleResponseStatusException(ResponseStatusException ex, WebRequest request) { diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/controller/ChatController.java b/server/src/main/java/com/usatiuk/tjv/y/server/controller/ChatController.java index b2fc680..79f1117 100644 --- a/server/src/main/java/com/usatiuk/tjv/y/server/controller/ChatController.java +++ b/server/src/main/java/com/usatiuk/tjv/y/server/controller/ChatController.java @@ -1,9 +1,12 @@ package com.usatiuk.tjv.y.server.controller; +import com.usatiuk.tjv.y.server.controller.annotations.*; import com.usatiuk.tjv.y.server.dto.ChatCreateTo; import com.usatiuk.tjv.y.server.dto.ChatTo; import com.usatiuk.tjv.y.server.dto.PersonTo; import com.usatiuk.tjv.y.server.service.ChatService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; @@ -11,6 +14,7 @@ import org.springframework.web.bind.annotation.*; import java.util.Collection; + @RestController @RequestMapping(value = "/chat", produces = MediaType.APPLICATION_JSON_VALUE) public class ChatController { @@ -21,32 +25,61 @@ public class ChatController { } @PostMapping + @Operation(summary = "Create a new chat, members must be at least the creator and someone else") + @ApiUnauthorizedResponse + @ApiBadRequestResponse + @ChatToResponse public ChatTo create(Authentication authentication, @RequestBody ChatCreateTo chatCreateTo) { return chatService.create(authentication, chatCreateTo); } @GetMapping(path = "/by-id/{id}") + @Operation(summary = "Get a chat by id, should be its member") + @ApiUnauthorizedResponse + @ApiNotFoundResponse + @ApiForbiddenResponse + @ChatToResponse public ChatTo get(@PathVariable Long id) { return chatService.getById(id); } @DeleteMapping(path = "/by-id/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) + @Operation(summary = "Delete a chat by id, should be its creator") + @ApiUnauthorizedResponse + @ApiNotFoundResponse + @ApiForbiddenResponse + @ApiResponse public void delete(@PathVariable Long id) { chatService.deleteById(id); } @PatchMapping(path = "/by-id/{id}") + @Operation(summary = "Update a chat by id, should be its creator, members must be at least the creator and someone else") + @ApiUnauthorizedResponse + @ApiNotFoundResponse + @ApiForbiddenResponse + @ApiBadRequestResponse + @ChatToResponse public ChatTo update(Authentication authentication, @PathVariable Long id, @RequestBody ChatCreateTo chatCreateTo) { return chatService.update(authentication, id, chatCreateTo); } @GetMapping(path = "/my") + @ApiUnauthorizedResponse + @ChatToArrResponse + @Operation(summary = "Get chats token holder is member of") public Collection getMy(Authentication authentication) { return chatService.getMy(authentication); } @GetMapping(path = "/by-id/{id}/members") + @ApiUnauthorizedResponse + @ApiNotFoundResponse + @ApiForbiddenResponse + @ApiBadRequestResponse + @PersonToArrResponse + @Operation(summary = "Get members of chat by id, should be its member") public Collection getMembers(@PathVariable Long id) { return chatService.getMembers(id); } diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/controller/MessageController.java b/server/src/main/java/com/usatiuk/tjv/y/server/controller/MessageController.java index b38a38a..426a20c 100644 --- a/server/src/main/java/com/usatiuk/tjv/y/server/controller/MessageController.java +++ b/server/src/main/java/com/usatiuk/tjv/y/server/controller/MessageController.java @@ -1,8 +1,10 @@ package com.usatiuk.tjv.y.server.controller; +import com.usatiuk.tjv.y.server.controller.annotations.*; import com.usatiuk.tjv.y.server.dto.MessageCreateTo; import com.usatiuk.tjv.y.server.dto.MessageTo; import com.usatiuk.tjv.y.server.service.MessageService; +import io.swagger.v3.oas.annotations.Operation; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; @@ -20,27 +22,52 @@ public class MessageController { } @GetMapping(path = "/by-chat/{chatTd}") - public Collection get(Authentication authentication, @PathVariable Long chatTd) { + @Operation(summary = "Get messages in a chat, must be its member") + @ApiUnauthorizedResponse + @ApiForbiddenResponse + @ApiNotFoundResponse + @MessageToArrResponse + public Collection get(@PathVariable Long chatTd) { return messageService.getByChat(chatTd); } @PostMapping(path = "/by-chat/{chatId}") + @Operation(summary = "Add a message to a chat, must be its member") + @ApiUnauthorizedResponse + @ApiForbiddenResponse + @ApiNotFoundResponse + @MessageToResponse + @ApiBadRequestResponse public MessageTo post(Authentication authentication, @PathVariable Long chatId, @RequestBody MessageCreateTo messageCreateTo) { return messageService.addToChat(authentication, chatId, messageCreateTo); } @PatchMapping(path = "/by-id/{id}") + @Operation(summary = "Change contents of a message, must be its author") + @ApiUnauthorizedResponse + @ApiForbiddenResponse + @ApiNotFoundResponse + @MessageToResponse + @ApiBadRequestResponse public MessageTo update(@PathVariable long id, @RequestBody MessageCreateTo messageCreateTo) { return messageService.update(id, messageCreateTo); } @DeleteMapping(path = "/by-id/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) + @Operation(summary = "Delete a message, must be its author") + @ApiUnauthorizedResponse + @ApiForbiddenResponse + @ApiNotFoundResponse public void delete(@PathVariable long id) { messageService.delete(id); } @GetMapping + @Operation(summary = "Get all messages, must be admin") + @ApiUnauthorizedResponse + @ApiForbiddenResponse + @MessageToArrResponse public Collection getAll() { return messageService.readAll(); } diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/controller/PersonController.java b/server/src/main/java/com/usatiuk/tjv/y/server/controller/PersonController.java index a7ddec2..aaa5a18 100644 --- a/server/src/main/java/com/usatiuk/tjv/y/server/controller/PersonController.java +++ b/server/src/main/java/com/usatiuk/tjv/y/server/controller/PersonController.java @@ -1,10 +1,10 @@ package com.usatiuk.tjv.y.server.controller; +import com.usatiuk.tjv.y.server.controller.annotations.*; import com.usatiuk.tjv.y.server.dto.PersonCreateTo; import com.usatiuk.tjv.y.server.dto.PersonTo; import com.usatiuk.tjv.y.server.service.PersonService; -import com.usatiuk.tjv.y.server.service.exceptions.ConflictException; -import com.usatiuk.tjv.y.server.service.exceptions.NotFoundException; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirements; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -24,84 +24,134 @@ public class PersonController { @PostMapping @SecurityRequirements(value = {}) - public PersonTo signup(@RequestBody PersonCreateTo signupRequest) throws ConflictException { + @Operation(summary = "Create a user, user created if there are no admins becomes an admin") + @PersonToResponse + @ApiBadRequestResponse + public PersonTo signup(@RequestBody PersonCreateTo signupRequest) { return personService.signup(signupRequest); } @GetMapping(path = "/by-username/{username}") - public PersonTo getByUsername(@PathVariable String username) throws NotFoundException { + @Operation(summary = "Get a user by username") + @PersonToResponse + @ApiNotFoundResponse + @ApiUnauthorizedResponse + public PersonTo getByUsername(@PathVariable String username) { return personService.readByUsername(username); } @GetMapping(path = "/by-uuid/{uuid}") - public PersonTo getByUuid(@PathVariable String uuid) throws NotFoundException { + @Operation(summary = "Get a user by uuid") + @PersonToResponse + @ApiNotFoundResponse + @ApiUnauthorizedResponse + public PersonTo getByUuid(@PathVariable String uuid) { return personService.readByUuid(uuid); } @GetMapping(path = "/self") - public PersonTo getSelf(Authentication authentication) throws NotFoundException { + @Operation(summary = "Get self") + @PersonToResponse + @ApiUnauthorizedResponse + public PersonTo getSelf(Authentication authentication) { return personService.readSelf(authentication); } @PatchMapping(path = "/self") + @Operation(summary = "Update self") + @PersonToResponse + @ApiUnauthorizedResponse + @ApiBadRequestResponse public PersonTo update(Authentication authentication, @RequestBody PersonCreateTo personCreateTo) { return personService.update(authentication, personCreateTo); } @DeleteMapping(path = "/self") @ResponseStatus(HttpStatus.NO_CONTENT) + @Operation(summary = "Delete self") + @ApiUnauthorizedResponse public void delete(Authentication authentication) { personService.deleteSelf(authentication); } @DeleteMapping(path = "/by-uuid/{uuid}") @ResponseStatus(HttpStatus.NO_CONTENT) - public void deleteByUuid(@PathVariable String uuid) throws NotFoundException { + @Operation(summary = "Delete a user by uuid, must be admin") + @ApiUnauthorizedResponse + @ApiNotFoundResponse + @ApiForbiddenResponse + public void deleteByUuid(@PathVariable String uuid) { personService.deleteByUuid(uuid); } @GetMapping - public Collection getAll() throws NotFoundException { + @Operation(summary = "Get all users") + @ApiUnauthorizedResponse + @PersonToArrResponse + public Collection getAll() { return personService.readAll(); } @GetMapping(path = "/followers") - public Collection getFollowers(Authentication authentication) throws NotFoundException { + @Operation(summary = "Get your followers") + @ApiUnauthorizedResponse + @PersonToArrResponse + public Collection getFollowers(Authentication authentication) { return personService.getFollowers(authentication); } @GetMapping(path = "/following") - public Collection getFollowing(Authentication authentication) throws NotFoundException { + @Operation(summary = "Get who you are following") + @ApiUnauthorizedResponse + @PersonToArrResponse + public Collection getFollowing(Authentication authentication) { return personService.getFollowing(authentication); } @GetMapping(path = "/admins") + @Operation(summary = "Get a list of admins") + @ApiUnauthorizedResponse + @PersonToArrResponse public Collection getAdmins() { return personService.getAdmins(); } @PutMapping(path = "/admins/{uuid}") @ResponseStatus(HttpStatus.NO_CONTENT) - public void addAdmin(@PathVariable String uuid) throws NotFoundException { + @Operation(summary = "Add an admin, must be admin self") + @ApiUnauthorizedResponse + @ApiForbiddenResponse + @ApiNotFoundResponse + public void addAdmin(@PathVariable String uuid) { personService.addAdmin(uuid); } @DeleteMapping(path = "/admins/{uuid}") @ResponseStatus(HttpStatus.NO_CONTENT) - public void deleteAdmin(@PathVariable String uuid) throws NotFoundException { + @Operation(summary = "Remove an admin, must be admin self") + @ApiUnauthorizedResponse + @ApiForbiddenResponse + @ApiNotFoundResponse + public void deleteAdmin(@PathVariable String uuid) { personService.removeAdmin(uuid); } @PutMapping(path = "/following/{uuid}") @ResponseStatus(HttpStatus.NO_CONTENT) - public void addFollowing(Authentication authentication, @PathVariable String uuid) throws NotFoundException { + @Operation(summary = "Follow someone") + @ApiUnauthorizedResponse + @ApiNotFoundResponse + public void addFollowing(Authentication authentication, @PathVariable String uuid) { personService.addFollower(authentication, uuid); } @DeleteMapping(path = "/following/{uuid}") @ResponseStatus(HttpStatus.NO_CONTENT) - public void deleteFollowing(Authentication authentication, @PathVariable String uuid) throws NotFoundException { + @Operation(summary = "Unfollow someone") + @ApiUnauthorizedResponse + @ApiNotFoundResponse + public void deleteFollowing(Authentication authentication, @PathVariable String uuid) { personService.removeFollower(authentication, uuid); } diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/controller/PostController.java b/server/src/main/java/com/usatiuk/tjv/y/server/controller/PostController.java index 813f4a7..f72d25f 100644 --- a/server/src/main/java/com/usatiuk/tjv/y/server/controller/PostController.java +++ b/server/src/main/java/com/usatiuk/tjv/y/server/controller/PostController.java @@ -1,9 +1,11 @@ package com.usatiuk.tjv.y.server.controller; +import com.usatiuk.tjv.y.server.controller.annotations.*; import com.usatiuk.tjv.y.server.dto.PostCreateTo; import com.usatiuk.tjv.y.server.dto.PostTo; import com.usatiuk.tjv.y.server.dto.converters.PostMapper; import com.usatiuk.tjv.y.server.service.PostService; +import io.swagger.v3.oas.annotations.Operation; import jakarta.persistence.EntityManager; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -22,42 +24,73 @@ public class PostController { } @PostMapping + @Operation(summary = "Create a post") + @PostToResponse + @ApiUnauthorizedResponse + @ApiBadRequestResponse public PostTo createPost(Authentication authentication, @RequestBody PostCreateTo postCreateTo) { return postService.createPost(authentication, postCreateTo); } @GetMapping(path = "/by-author-uuid/{uuid}") + @Operation(summary = "Read all posts by some author by their uuid") + @PostToArrResponse + @ApiNotFoundResponse + @ApiUnauthorizedResponse public Collection readAllByAuthorUuid(@PathVariable String uuid) { return postService.readByAuthorId(uuid); } @GetMapping(path = "/by-author-username/{username}") + @Operation(summary = "Read all posts by some author by their username") + @PostToArrResponse + @ApiNotFoundResponse + @ApiUnauthorizedResponse public Collection readAllByAuthorUsername(@PathVariable String username) { return postService.readByAuthorUsername(username); } @GetMapping(path = "/by-following") + @Operation(summary = "Read all posts by authors you're following") + @PostToArrResponse + @ApiUnauthorizedResponse public Collection readAllByFollowees(Authentication authentication) { return postService.readByPersonFollowees(authentication); } @GetMapping(path = "/{id}") + @Operation(summary = "Read a post by id") + @PostToResponse + @ApiUnauthorizedResponse + @ApiNotFoundResponse public PostTo get(@PathVariable long id) { return postService.readById(id); } @PatchMapping(path = "/{id}") + @Operation(summary = "Update a post, must be its author") + @PostToResponse + @ApiUnauthorizedResponse + @ApiBadRequestResponse + @ApiForbiddenResponse public PostTo update(@PathVariable long id, @RequestBody PostCreateTo postCreateTo) { return postService.updatePost(id, postCreateTo); } @DeleteMapping(path = "/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) + @Operation(summary = "Delete a post, must be its author") + @ApiUnauthorizedResponse + @ApiForbiddenResponse public void delete(@PathVariable long id) { postService.deletePost(id); } @GetMapping + @Operation(summary = "Get all posts, must be admin") + @ApiUnauthorizedResponse + @ApiForbiddenResponse + @PostToArrResponse public Collection getAll() { return postService.readAll(); } diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/controller/TokenController.java b/server/src/main/java/com/usatiuk/tjv/y/server/controller/TokenController.java index d371b33..fb4ada5 100644 --- a/server/src/main/java/com/usatiuk/tjv/y/server/controller/TokenController.java +++ b/server/src/main/java/com/usatiuk/tjv/y/server/controller/TokenController.java @@ -1,8 +1,13 @@ package com.usatiuk.tjv.y.server.controller; +import com.usatiuk.tjv.y.server.controller.annotations.ApiUnauthorizedResponse; import com.usatiuk.tjv.y.server.dto.TokenRequestTo; import com.usatiuk.tjv.y.server.dto.TokenResponseTo; import com.usatiuk.tjv.y.server.service.LoginTokenService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.security.SecurityRequirements; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.PostMapping; @@ -21,6 +26,15 @@ public class TokenController { @PostMapping @SecurityRequirements(value = {}) + @Operation(summary = "Get a token (login)") + @ApiUnauthorizedResponse + @ApiResponse( + responseCode = "200", + description = "Returns a token", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = TokenResponseTo.class) + )) public TokenResponseTo request(@RequestBody TokenRequestTo tokenRequestTo) { return loginTokenService.login(tokenRequestTo); } diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/ApiBadRequestResponse.java b/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/ApiBadRequestResponse.java new file mode 100644 index 0000000..fe38ec8 --- /dev/null +++ b/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/ApiBadRequestResponse.java @@ -0,0 +1,24 @@ +package com.usatiuk.tjv.y.server.controller.annotations; + +import com.usatiuk.tjv.y.server.dto.ErrorTo; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@ApiResponse( + responseCode = "400", + description = "Bad request - input is invalid, details are in the error response", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorTo.class) + )) +public @interface ApiBadRequestResponse { + +} diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/ApiConflictResponse.java b/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/ApiConflictResponse.java new file mode 100644 index 0000000..40b75d5 --- /dev/null +++ b/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/ApiConflictResponse.java @@ -0,0 +1,24 @@ +package com.usatiuk.tjv.y.server.controller.annotations; + +import com.usatiuk.tjv.y.server.dto.ErrorTo; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@ApiResponse( + responseCode = "409", + description = "Conflict - creating/updating what is requested would conflict with existing data", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorTo.class) + )) +public @interface ApiConflictResponse { + +} diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/ApiForbiddenResponse.java b/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/ApiForbiddenResponse.java new file mode 100644 index 0000000..8c5317a --- /dev/null +++ b/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/ApiForbiddenResponse.java @@ -0,0 +1,24 @@ +package com.usatiuk.tjv.y.server.controller.annotations; + +import com.usatiuk.tjv.y.server.dto.ErrorTo; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@ApiResponse( + responseCode = "403", + description = "Forbidden - the token holder doesn't have the rights for this action", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorTo.class) + )) +public @interface ApiForbiddenResponse { + +} diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/ApiNotFoundResponse.java b/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/ApiNotFoundResponse.java new file mode 100644 index 0000000..d24378b --- /dev/null +++ b/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/ApiNotFoundResponse.java @@ -0,0 +1,24 @@ +package com.usatiuk.tjv.y.server.controller.annotations; + +import com.usatiuk.tjv.y.server.dto.ErrorTo; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@ApiResponse( + responseCode = "404", + description = "Not found - one of the resources mentioned in the request doesn't exist", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorTo.class) + )) +public @interface ApiNotFoundResponse { + +} diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/ApiUnauthorizedResponse.java b/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/ApiUnauthorizedResponse.java new file mode 100644 index 0000000..8b9442a --- /dev/null +++ b/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/ApiUnauthorizedResponse.java @@ -0,0 +1,24 @@ +package com.usatiuk.tjv.y.server.controller.annotations; + +import com.usatiuk.tjv.y.server.dto.ErrorTo; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@ApiResponse( + responseCode = "401", + description = "Unauthorized - requires a valid auth token", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorTo.class) + )) +public @interface ApiUnauthorizedResponse { + +} diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/ChatToArrResponse.java b/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/ChatToArrResponse.java new file mode 100644 index 0000000..ce68c4e --- /dev/null +++ b/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/ChatToArrResponse.java @@ -0,0 +1,22 @@ +package com.usatiuk.tjv.y.server.controller.annotations; + +import com.usatiuk.tjv.y.server.dto.ChatTo; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@ApiResponse( + responseCode = "200", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ChatTo[].class) + )) +public @interface ChatToArrResponse { +} diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/ChatToResponse.java b/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/ChatToResponse.java new file mode 100644 index 0000000..737cf86 --- /dev/null +++ b/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/ChatToResponse.java @@ -0,0 +1,23 @@ +package com.usatiuk.tjv.y.server.controller.annotations; + +import com.usatiuk.tjv.y.server.dto.ChatTo; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@ApiResponse( + responseCode = "200", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ChatTo.class) + )) +public @interface ChatToResponse { +} + diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/MessageToArrResponse.java b/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/MessageToArrResponse.java new file mode 100644 index 0000000..e12a5ec --- /dev/null +++ b/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/MessageToArrResponse.java @@ -0,0 +1,22 @@ +package com.usatiuk.tjv.y.server.controller.annotations; + +import com.usatiuk.tjv.y.server.dto.MessageTo; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@ApiResponse( + responseCode = "200", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = MessageTo[].class) + )) +public @interface MessageToArrResponse { +} diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/MessageToResponse.java b/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/MessageToResponse.java new file mode 100644 index 0000000..090312c --- /dev/null +++ b/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/MessageToResponse.java @@ -0,0 +1,22 @@ +package com.usatiuk.tjv.y.server.controller.annotations; + +import com.usatiuk.tjv.y.server.dto.MessageTo; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@ApiResponse( + responseCode = "200", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = MessageTo.class) + )) +public @interface MessageToResponse { +} diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/PersonToArrResponse.java b/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/PersonToArrResponse.java new file mode 100644 index 0000000..2cd2faa --- /dev/null +++ b/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/PersonToArrResponse.java @@ -0,0 +1,22 @@ +package com.usatiuk.tjv.y.server.controller.annotations; + +import com.usatiuk.tjv.y.server.dto.PersonTo; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@ApiResponse( + responseCode = "200", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = PersonTo[].class) + )) +public @interface PersonToArrResponse { +} diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/PersonToResponse.java b/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/PersonToResponse.java new file mode 100644 index 0000000..d4eb94a --- /dev/null +++ b/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/PersonToResponse.java @@ -0,0 +1,24 @@ +package com.usatiuk.tjv.y.server.controller.annotations; + +import com.usatiuk.tjv.y.server.dto.PersonTo; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@ApiResponse( + responseCode = "200", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = PersonTo.class) + )) +public @interface PersonToResponse { +} + + diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/PostToArrResponse.java b/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/PostToArrResponse.java new file mode 100644 index 0000000..f343137 --- /dev/null +++ b/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/PostToArrResponse.java @@ -0,0 +1,22 @@ +package com.usatiuk.tjv.y.server.controller.annotations; + +import com.usatiuk.tjv.y.server.dto.PostTo; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@ApiResponse( + responseCode = "200", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = PostTo[].class) + )) +public @interface PostToArrResponse { +} diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/PostToResponse.java b/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/PostToResponse.java new file mode 100644 index 0000000..48023ed --- /dev/null +++ b/server/src/main/java/com/usatiuk/tjv/y/server/controller/annotations/PostToResponse.java @@ -0,0 +1,22 @@ +package com.usatiuk.tjv.y.server.controller.annotations; + +import com.usatiuk.tjv.y.server.entity.Post; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@ApiResponse( + responseCode = "200", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = Post.class) + )) +public @interface PostToResponse { +} diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/dto/ChatCreateTo.java b/server/src/main/java/com/usatiuk/tjv/y/server/dto/ChatCreateTo.java index f05cd8f..0c4e215 100644 --- a/server/src/main/java/com/usatiuk/tjv/y/server/dto/ChatCreateTo.java +++ b/server/src/main/java/com/usatiuk/tjv/y/server/dto/ChatCreateTo.java @@ -1,4 +1,8 @@ package com.usatiuk.tjv.y.server.dto; -public record ChatCreateTo(String name, String[] memberUuids) { +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record ChatCreateTo(@NotBlank String name, + @NotBlank @Size(min = 2) String[] memberUuids) { } diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/dto/MessageCreateTo.java b/server/src/main/java/com/usatiuk/tjv/y/server/dto/MessageCreateTo.java index 5ecc34d..c193f01 100644 --- a/server/src/main/java/com/usatiuk/tjv/y/server/dto/MessageCreateTo.java +++ b/server/src/main/java/com/usatiuk/tjv/y/server/dto/MessageCreateTo.java @@ -1,4 +1,6 @@ package com.usatiuk.tjv.y.server.dto; -public record MessageCreateTo(String contents) { +import jakarta.validation.constraints.NotBlank; + +public record MessageCreateTo(@NotBlank String contents) { } diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/dto/PersonCreateTo.java b/server/src/main/java/com/usatiuk/tjv/y/server/dto/PersonCreateTo.java index ab17c37..6498ecb 100644 --- a/server/src/main/java/com/usatiuk/tjv/y/server/dto/PersonCreateTo.java +++ b/server/src/main/java/com/usatiuk/tjv/y/server/dto/PersonCreateTo.java @@ -1,4 +1,8 @@ package com.usatiuk.tjv.y.server.dto; -public record PersonCreateTo(String username, String fullName, String password) { +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record PersonCreateTo(@NotBlank @Size(max = 100) String username, @NotBlank @Size(max = 100) String fullName, + @NotBlank String password) { } diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/dto/PostCreateTo.java b/server/src/main/java/com/usatiuk/tjv/y/server/dto/PostCreateTo.java index 59702bb..65a0a8e 100644 --- a/server/src/main/java/com/usatiuk/tjv/y/server/dto/PostCreateTo.java +++ b/server/src/main/java/com/usatiuk/tjv/y/server/dto/PostCreateTo.java @@ -1,4 +1,6 @@ package com.usatiuk.tjv.y.server.dto; -public record PostCreateTo(String text) { +import jakarta.validation.constraints.NotBlank; + +public record PostCreateTo(@NotBlank String text) { } diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/dto/TokenRequestTo.java b/server/src/main/java/com/usatiuk/tjv/y/server/dto/TokenRequestTo.java index a0699ee..eb46c20 100644 --- a/server/src/main/java/com/usatiuk/tjv/y/server/dto/TokenRequestTo.java +++ b/server/src/main/java/com/usatiuk/tjv/y/server/dto/TokenRequestTo.java @@ -1,4 +1,6 @@ package com.usatiuk.tjv.y.server.dto; -public record TokenRequestTo(String username, String password) { +import jakarta.validation.constraints.NotBlank; + +public record TokenRequestTo(@NotBlank String username, @NotBlank String password) { } diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/service/ChatService.java b/server/src/main/java/com/usatiuk/tjv/y/server/service/ChatService.java index 5e9ff38..1394d0f 100644 --- a/server/src/main/java/com/usatiuk/tjv/y/server/service/ChatService.java +++ b/server/src/main/java/com/usatiuk/tjv/y/server/service/ChatService.java @@ -18,7 +18,7 @@ public interface ChatService { @PreAuthorize("@chatService.isMemberOf(authentication.principal.username, #id)") ChatTo getById(Long id); - @PreAuthorize("@chatService.isMemberOf(authentication.principal.username, #id)") + @PreAuthorize("@chatService.isCreatorOf(authentication.principal.username, #id)") void deleteById(Long id); @PreAuthorize("@chatService.isMemberOf(authentication.principal.username, #id)") diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/spasupport/WebConfig.java b/server/src/main/java/com/usatiuk/tjv/y/server/spasupport/WebConfig.java index c95df4d..a9979ad 100644 --- a/server/src/main/java/com/usatiuk/tjv/y/server/spasupport/WebConfig.java +++ b/server/src/main/java/com/usatiuk/tjv/y/server/spasupport/WebConfig.java @@ -10,6 +10,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration @EnableWebMvc @Profile("prod") +// A bit of a mess, but even then it seems to be the simplest way to serve a single page application... public class WebConfig implements WebMvcConfigurer { private final AppResourceResolver appResourceResolver;