somewhat documented API

This commit is contained in:
Stepan Usatiuk
2024-01-04 17:18:01 +01:00
parent b694c9d377
commit 72161902f9
28 changed files with 509 additions and 54 deletions

View File

@@ -29,11 +29,9 @@ services:
- ymariadb:/var/lib/mysql - ymariadb:/var/lib/mysql
healthcheck: healthcheck:
test: [ "CMD", "healthcheck.sh", "--connect", "--innodb_initialized" ] test: [ "CMD", "healthcheck.sh", "--connect", "--innodb_initialized" ]
start_period: 30s
start_interval: 10s
interval: 5s interval: 5s
timeout: 5s timeout: 5s
retries: 3 retries: 10
volumes: volumes:
ymariadb: ymariadb:

View File

@@ -1,19 +1,10 @@
package com.usatiuk.tjv.y.server; 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.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.info.Info; import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.security.SecurityScheme; 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; import org.springframework.context.annotation.Configuration;
@Configuration @Configuration
@@ -25,24 +16,4 @@ import org.springframework.context.annotation.Configuration;
scheme = "bearer" scheme = "bearer"
) )
public class OpenAPIConfiguration { 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));
}));
};
}
} }

View File

@@ -7,9 +7,11 @@ import com.usatiuk.tjv.y.server.service.exceptions.NotFoundException;
import jakarta.persistence.EntityNotFoundException; import jakarta.persistence.EntityNotFoundException;
import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException; import jakarta.validation.ConstraintViolationException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.orm.jpa.JpaObjectRetrievalFailureException;
import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.UsernameNotFoundException;
@@ -43,6 +45,13 @@ public class ApiExceptionHandler extends ResponseEntityExceptionHandler {
} }
} }
@ExceptionHandler(DataIntegrityViolationException.class)
protected ResponseEntity<Object> 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) @ExceptionHandler(AuthenticationException.class)
protected ResponseEntity<Object> handleAuthenticationException(AuthenticationException ex, WebRequest request) { protected ResponseEntity<Object> handleAuthenticationException(AuthenticationException ex, WebRequest request) {
return handleExceptionInternal(ex, return handleExceptionInternal(ex,
@@ -92,6 +101,12 @@ public class ApiExceptionHandler extends ResponseEntityExceptionHandler {
new HttpHeaders(), HttpStatus.NOT_FOUND, request); new HttpHeaders(), HttpStatus.NOT_FOUND, request);
} }
@ExceptionHandler(JpaObjectRetrievalFailureException.class)
protected ResponseEntity<Object> 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) @ExceptionHandler(ResponseStatusException.class)
protected ResponseEntity<Object> handleResponseStatusException(ResponseStatusException ex, WebRequest request) { protected ResponseEntity<Object> handleResponseStatusException(ResponseStatusException ex, WebRequest request) {

View File

@@ -1,9 +1,12 @@
package com.usatiuk.tjv.y.server.controller; 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.ChatCreateTo;
import com.usatiuk.tjv.y.server.dto.ChatTo; import com.usatiuk.tjv.y.server.dto.ChatTo;
import com.usatiuk.tjv.y.server.dto.PersonTo; import com.usatiuk.tjv.y.server.dto.PersonTo;
import com.usatiuk.tjv.y.server.service.ChatService; 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.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@@ -11,6 +14,7 @@ import org.springframework.web.bind.annotation.*;
import java.util.Collection; import java.util.Collection;
@RestController @RestController
@RequestMapping(value = "/chat", produces = MediaType.APPLICATION_JSON_VALUE) @RequestMapping(value = "/chat", produces = MediaType.APPLICATION_JSON_VALUE)
public class ChatController { public class ChatController {
@@ -21,32 +25,61 @@ public class ChatController {
} }
@PostMapping @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) { public ChatTo create(Authentication authentication, @RequestBody ChatCreateTo chatCreateTo) {
return chatService.create(authentication, chatCreateTo); return chatService.create(authentication, chatCreateTo);
} }
@GetMapping(path = "/by-id/{id}") @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) { public ChatTo get(@PathVariable Long id) {
return chatService.getById(id); return chatService.getById(id);
} }
@DeleteMapping(path = "/by-id/{id}") @DeleteMapping(path = "/by-id/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT) @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) { public void delete(@PathVariable Long id) {
chatService.deleteById(id); chatService.deleteById(id);
} }
@PatchMapping(path = "/by-id/{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) { public ChatTo update(Authentication authentication, @PathVariable Long id, @RequestBody ChatCreateTo chatCreateTo) {
return chatService.update(authentication, id, chatCreateTo); return chatService.update(authentication, id, chatCreateTo);
} }
@GetMapping(path = "/my") @GetMapping(path = "/my")
@ApiUnauthorizedResponse
@ChatToArrResponse
@Operation(summary = "Get chats token holder is member of")
public Collection<ChatTo> getMy(Authentication authentication) { public Collection<ChatTo> getMy(Authentication authentication) {
return chatService.getMy(authentication); return chatService.getMy(authentication);
} }
@GetMapping(path = "/by-id/{id}/members") @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<PersonTo> getMembers(@PathVariable Long id) { public Collection<PersonTo> getMembers(@PathVariable Long id) {
return chatService.getMembers(id); return chatService.getMembers(id);
} }

View File

@@ -1,8 +1,10 @@
package com.usatiuk.tjv.y.server.controller; 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.MessageCreateTo;
import com.usatiuk.tjv.y.server.dto.MessageTo; import com.usatiuk.tjv.y.server.dto.MessageTo;
import com.usatiuk.tjv.y.server.service.MessageService; import com.usatiuk.tjv.y.server.service.MessageService;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@@ -20,27 +22,52 @@ public class MessageController {
} }
@GetMapping(path = "/by-chat/{chatTd}") @GetMapping(path = "/by-chat/{chatTd}")
public Collection<MessageTo> get(Authentication authentication, @PathVariable Long chatTd) { @Operation(summary = "Get messages in a chat, must be its member")
@ApiUnauthorizedResponse
@ApiForbiddenResponse
@ApiNotFoundResponse
@MessageToArrResponse
public Collection<MessageTo> get(@PathVariable Long chatTd) {
return messageService.getByChat(chatTd); return messageService.getByChat(chatTd);
} }
@PostMapping(path = "/by-chat/{chatId}") @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) { public MessageTo post(Authentication authentication, @PathVariable Long chatId, @RequestBody MessageCreateTo messageCreateTo) {
return messageService.addToChat(authentication, chatId, messageCreateTo); return messageService.addToChat(authentication, chatId, messageCreateTo);
} }
@PatchMapping(path = "/by-id/{id}") @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) { public MessageTo update(@PathVariable long id, @RequestBody MessageCreateTo messageCreateTo) {
return messageService.update(id, messageCreateTo); return messageService.update(id, messageCreateTo);
} }
@DeleteMapping(path = "/by-id/{id}") @DeleteMapping(path = "/by-id/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT) @ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(summary = "Delete a message, must be its author")
@ApiUnauthorizedResponse
@ApiForbiddenResponse
@ApiNotFoundResponse
public void delete(@PathVariable long id) { public void delete(@PathVariable long id) {
messageService.delete(id); messageService.delete(id);
} }
@GetMapping @GetMapping
@Operation(summary = "Get all messages, must be admin")
@ApiUnauthorizedResponse
@ApiForbiddenResponse
@MessageToArrResponse
public Collection<MessageTo> getAll() { public Collection<MessageTo> getAll() {
return messageService.readAll(); return messageService.readAll();
} }

View File

@@ -1,10 +1,10 @@
package com.usatiuk.tjv.y.server.controller; 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.PersonCreateTo;
import com.usatiuk.tjv.y.server.dto.PersonTo; import com.usatiuk.tjv.y.server.dto.PersonTo;
import com.usatiuk.tjv.y.server.service.PersonService; import com.usatiuk.tjv.y.server.service.PersonService;
import com.usatiuk.tjv.y.server.service.exceptions.ConflictException; import io.swagger.v3.oas.annotations.Operation;
import com.usatiuk.tjv.y.server.service.exceptions.NotFoundException;
import io.swagger.v3.oas.annotations.security.SecurityRequirements; import io.swagger.v3.oas.annotations.security.SecurityRequirements;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@@ -24,84 +24,134 @@ public class PersonController {
@PostMapping @PostMapping
@SecurityRequirements(value = {}) @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); return personService.signup(signupRequest);
} }
@GetMapping(path = "/by-username/{username}") @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); return personService.readByUsername(username);
} }
@GetMapping(path = "/by-uuid/{uuid}") @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); return personService.readByUuid(uuid);
} }
@GetMapping(path = "/self") @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); return personService.readSelf(authentication);
} }
@PatchMapping(path = "/self") @PatchMapping(path = "/self")
@Operation(summary = "Update self")
@PersonToResponse
@ApiUnauthorizedResponse
@ApiBadRequestResponse
public PersonTo update(Authentication authentication, @RequestBody PersonCreateTo personCreateTo) { public PersonTo update(Authentication authentication, @RequestBody PersonCreateTo personCreateTo) {
return personService.update(authentication, personCreateTo); return personService.update(authentication, personCreateTo);
} }
@DeleteMapping(path = "/self") @DeleteMapping(path = "/self")
@ResponseStatus(HttpStatus.NO_CONTENT) @ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(summary = "Delete self")
@ApiUnauthorizedResponse
public void delete(Authentication authentication) { public void delete(Authentication authentication) {
personService.deleteSelf(authentication); personService.deleteSelf(authentication);
} }
@DeleteMapping(path = "/by-uuid/{uuid}") @DeleteMapping(path = "/by-uuid/{uuid}")
@ResponseStatus(HttpStatus.NO_CONTENT) @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); personService.deleteByUuid(uuid);
} }
@GetMapping @GetMapping
public Collection<PersonTo> getAll() throws NotFoundException { @Operation(summary = "Get all users")
@ApiUnauthorizedResponse
@PersonToArrResponse
public Collection<PersonTo> getAll() {
return personService.readAll(); return personService.readAll();
} }
@GetMapping(path = "/followers") @GetMapping(path = "/followers")
public Collection<PersonTo> getFollowers(Authentication authentication) throws NotFoundException { @Operation(summary = "Get your followers")
@ApiUnauthorizedResponse
@PersonToArrResponse
public Collection<PersonTo> getFollowers(Authentication authentication) {
return personService.getFollowers(authentication); return personService.getFollowers(authentication);
} }
@GetMapping(path = "/following") @GetMapping(path = "/following")
public Collection<PersonTo> getFollowing(Authentication authentication) throws NotFoundException { @Operation(summary = "Get who you are following")
@ApiUnauthorizedResponse
@PersonToArrResponse
public Collection<PersonTo> getFollowing(Authentication authentication) {
return personService.getFollowing(authentication); return personService.getFollowing(authentication);
} }
@GetMapping(path = "/admins") @GetMapping(path = "/admins")
@Operation(summary = "Get a list of admins")
@ApiUnauthorizedResponse
@PersonToArrResponse
public Collection<PersonTo> getAdmins() { public Collection<PersonTo> getAdmins() {
return personService.getAdmins(); return personService.getAdmins();
} }
@PutMapping(path = "/admins/{uuid}") @PutMapping(path = "/admins/{uuid}")
@ResponseStatus(HttpStatus.NO_CONTENT) @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); personService.addAdmin(uuid);
} }
@DeleteMapping(path = "/admins/{uuid}") @DeleteMapping(path = "/admins/{uuid}")
@ResponseStatus(HttpStatus.NO_CONTENT) @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); personService.removeAdmin(uuid);
} }
@PutMapping(path = "/following/{uuid}") @PutMapping(path = "/following/{uuid}")
@ResponseStatus(HttpStatus.NO_CONTENT) @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); personService.addFollower(authentication, uuid);
} }
@DeleteMapping(path = "/following/{uuid}") @DeleteMapping(path = "/following/{uuid}")
@ResponseStatus(HttpStatus.NO_CONTENT) @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); personService.removeFollower(authentication, uuid);
} }

View File

@@ -1,9 +1,11 @@
package com.usatiuk.tjv.y.server.controller; 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.PostCreateTo;
import com.usatiuk.tjv.y.server.dto.PostTo; import com.usatiuk.tjv.y.server.dto.PostTo;
import com.usatiuk.tjv.y.server.dto.converters.PostMapper; import com.usatiuk.tjv.y.server.dto.converters.PostMapper;
import com.usatiuk.tjv.y.server.service.PostService; import com.usatiuk.tjv.y.server.service.PostService;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@@ -22,42 +24,73 @@ public class PostController {
} }
@PostMapping @PostMapping
@Operation(summary = "Create a post")
@PostToResponse
@ApiUnauthorizedResponse
@ApiBadRequestResponse
public PostTo createPost(Authentication authentication, @RequestBody PostCreateTo postCreateTo) { public PostTo createPost(Authentication authentication, @RequestBody PostCreateTo postCreateTo) {
return postService.createPost(authentication, postCreateTo); return postService.createPost(authentication, postCreateTo);
} }
@GetMapping(path = "/by-author-uuid/{uuid}") @GetMapping(path = "/by-author-uuid/{uuid}")
@Operation(summary = "Read all posts by some author by their uuid")
@PostToArrResponse
@ApiNotFoundResponse
@ApiUnauthorizedResponse
public Collection<PostTo> readAllByAuthorUuid(@PathVariable String uuid) { public Collection<PostTo> readAllByAuthorUuid(@PathVariable String uuid) {
return postService.readByAuthorId(uuid); return postService.readByAuthorId(uuid);
} }
@GetMapping(path = "/by-author-username/{username}") @GetMapping(path = "/by-author-username/{username}")
@Operation(summary = "Read all posts by some author by their username")
@PostToArrResponse
@ApiNotFoundResponse
@ApiUnauthorizedResponse
public Collection<PostTo> readAllByAuthorUsername(@PathVariable String username) { public Collection<PostTo> readAllByAuthorUsername(@PathVariable String username) {
return postService.readByAuthorUsername(username); return postService.readByAuthorUsername(username);
} }
@GetMapping(path = "/by-following") @GetMapping(path = "/by-following")
@Operation(summary = "Read all posts by authors you're following")
@PostToArrResponse
@ApiUnauthorizedResponse
public Collection<PostTo> readAllByFollowees(Authentication authentication) { public Collection<PostTo> readAllByFollowees(Authentication authentication) {
return postService.readByPersonFollowees(authentication); return postService.readByPersonFollowees(authentication);
} }
@GetMapping(path = "/{id}") @GetMapping(path = "/{id}")
@Operation(summary = "Read a post by id")
@PostToResponse
@ApiUnauthorizedResponse
@ApiNotFoundResponse
public PostTo get(@PathVariable long id) { public PostTo get(@PathVariable long id) {
return postService.readById(id); return postService.readById(id);
} }
@PatchMapping(path = "/{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) { public PostTo update(@PathVariable long id, @RequestBody PostCreateTo postCreateTo) {
return postService.updatePost(id, postCreateTo); return postService.updatePost(id, postCreateTo);
} }
@DeleteMapping(path = "/{id}") @DeleteMapping(path = "/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT) @ResponseStatus(HttpStatus.NO_CONTENT)
@Operation(summary = "Delete a post, must be its author")
@ApiUnauthorizedResponse
@ApiForbiddenResponse
public void delete(@PathVariable long id) { public void delete(@PathVariable long id) {
postService.deletePost(id); postService.deletePost(id);
} }
@GetMapping @GetMapping
@Operation(summary = "Get all posts, must be admin")
@ApiUnauthorizedResponse
@ApiForbiddenResponse
@PostToArrResponse
public Collection<PostTo> getAll() { public Collection<PostTo> getAll() {
return postService.readAll(); return postService.readAll();
} }

View File

@@ -1,8 +1,13 @@
package com.usatiuk.tjv.y.server.controller; 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.TokenRequestTo;
import com.usatiuk.tjv.y.server.dto.TokenResponseTo; import com.usatiuk.tjv.y.server.dto.TokenResponseTo;
import com.usatiuk.tjv.y.server.service.LoginTokenService; 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 io.swagger.v3.oas.annotations.security.SecurityRequirements;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@@ -21,6 +26,15 @@ public class TokenController {
@PostMapping @PostMapping
@SecurityRequirements(value = {}) @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) { public TokenResponseTo request(@RequestBody TokenRequestTo tokenRequestTo) {
return loginTokenService.login(tokenRequestTo); return loginTokenService.login(tokenRequestTo);
} }

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -1,4 +1,8 @@
package com.usatiuk.tjv.y.server.dto; 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) {
} }

View File

@@ -1,4 +1,6 @@
package com.usatiuk.tjv.y.server.dto; package com.usatiuk.tjv.y.server.dto;
public record MessageCreateTo(String contents) { import jakarta.validation.constraints.NotBlank;
public record MessageCreateTo(@NotBlank String contents) {
} }

View File

@@ -1,4 +1,8 @@
package com.usatiuk.tjv.y.server.dto; 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) {
} }

View File

@@ -1,4 +1,6 @@
package com.usatiuk.tjv.y.server.dto; package com.usatiuk.tjv.y.server.dto;
public record PostCreateTo(String text) { import jakarta.validation.constraints.NotBlank;
public record PostCreateTo(@NotBlank String text) {
} }

View File

@@ -1,4 +1,6 @@
package com.usatiuk.tjv.y.server.dto; 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) {
} }

View File

@@ -18,7 +18,7 @@ public interface ChatService {
@PreAuthorize("@chatService.isMemberOf(authentication.principal.username, #id)") @PreAuthorize("@chatService.isMemberOf(authentication.principal.username, #id)")
ChatTo getById(Long id); ChatTo getById(Long id);
@PreAuthorize("@chatService.isMemberOf(authentication.principal.username, #id)") @PreAuthorize("@chatService.isCreatorOf(authentication.principal.username, #id)")
void deleteById(Long id); void deleteById(Long id);
@PreAuthorize("@chatService.isMemberOf(authentication.principal.username, #id)") @PreAuthorize("@chatService.isMemberOf(authentication.principal.username, #id)")

View File

@@ -10,6 +10,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration @Configuration
@EnableWebMvc @EnableWebMvc
@Profile("prod") @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 { public class WebConfig implements WebMvcConfigurer {
private final AppResourceResolver appResourceResolver; private final AppResourceResolver appResourceResolver;