diff --git a/build.gradle.kts b/build.gradle.kts index 355fa5b..7660026 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,11 +1,11 @@ plugins { - id("org.springframework.boot") version "3.3.0" - id("io.spring.dependency-management") version "1.1.5" - kotlin("plugin.jpa") version "1.9.24" - kotlin("jvm") version "1.9.24" - kotlin("plugin.spring") version "1.9.24" - kotlin("plugin.allopen") version "1.9.22" - kotlin("kapt") version "1.9.10" + id("org.springframework.boot") version "3.5.12" + id("io.spring.dependency-management") version "1.1.7" + kotlin("plugin.jpa") version "2.3.20" + kotlin("jvm") version "2.3.20" + kotlin("plugin.spring") version "2.3.20" + kotlin("plugin.allopen") version "2.3.20" + kotlin("kapt") version "2.3.20" } group = "at.eisibaer" @@ -28,20 +28,19 @@ repositories { } val bcVersion: String = "1.78.1" -val mapstructVersion: String = "1.6.0" +val mapstructVersion: String = "1.7.0.Beta1" dependencies { implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-websocket") - implementation("org.springframework.boot:spring-boot-docker-compose") - implementation("org.springframework.boot:spring-boot-starter-data-redis") - implementation("org.springframework.session:spring-session-data-redis") + implementation("org.springframework.session:spring-session-jdbc") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.bouncycastle:bcprov-jdk18on:$bcVersion") implementation("org.mapstruct:mapstruct:$mapstructVersion") + implementation("org.jetbrains.kotlin:kotlin-metadata-jvm:2.3.20") compileOnly("org.projectlombok:lombok") developmentOnly("org.springframework.boot:spring-boot-devtools") runtimeOnly("org.postgresql:postgresql") diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 0c32db6..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,7 +0,0 @@ -version: "3.1" - -services: - redis: - image: "redis:alpine" - ports: - - "6379:6379" \ No newline at end of file diff --git a/src/main/kotlin/at/eisibaer/jbear2/endpoint/AuthEndpoint.kt b/src/main/kotlin/at/eisibaer/jbear2/endpoint/AuthEndpoint.kt index 88fddbc..049951a 100644 --- a/src/main/kotlin/at/eisibaer/jbear2/endpoint/AuthEndpoint.kt +++ b/src/main/kotlin/at/eisibaer/jbear2/endpoint/AuthEndpoint.kt @@ -11,10 +11,7 @@ import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.http.ResponseEntity import org.springframework.security.authentication.AuthenticationManager -import org.springframework.security.authentication.BadCredentialsException import org.springframework.security.authentication.UsernamePasswordAuthenticationToken -import org.springframework.security.core.Authentication -import org.springframework.security.core.AuthenticationException import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.stereotype.Controller @@ -31,24 +28,24 @@ class AuthEndpoint( val encoder: PasswordEncoder, ) { - private val log: Logger = LoggerFactory.getLogger(AuthEndpoint::class.java); + private val log: Logger = LoggerFactory.getLogger(AuthEndpoint::class.java) - val strResponseSuccess: String = "Sending back success response"; - val strAlreadyLoggedIn: String = "User already logged in"; + val strResponseSuccess: String = "Sending back success response" + val strAlreadyLoggedIn: String = "User already logged in" @PostMapping("/signup") fun signupUser(@RequestBody loginDto: LoginDto, session: HttpSession): ResponseEntity<*>{ - log.info("Endpoint signupUser called"); - log.debug("signup Request with username: {}", loginDto.username); + log.info("Endpoint signupUser called") + log.debug("signup Request with username: {}", loginDto.username) if( userRepository.existsByUsername(loginDto.username)){ - log.info("Username was already taken"); - return ResponseEntity.badRequest().body("Username already taken"); + log.info("Username was already taken") + return ResponseEntity.badRequest().body("Username already taken") } - val user = User(loginDto.username, encoder.encode( loginDto.password), ArrayList(), null, null ); + val user = User(loginDto.username, encoder.encode( loginDto.password), ArrayList(), null, null ) - userRepository.save(user); + userRepository.save(user) val authentication = authenticationManager.authenticate( UsernamePasswordAuthenticationToken( @@ -57,81 +54,69 @@ class AuthEndpoint( ) ) - SecurityContextHolder.getContext().authentication = authentication; + SecurityContextHolder.getContext().authentication = authentication - val userDetails: UserDetailsImpl = authentication.principal as UserDetailsImpl; + val userDetails: UserDetailsImpl = authentication.principal as UserDetailsImpl - session.setAttribute(STR_SESSION_USER_KEY, userDetails); + session.setAttribute(STR_SESSION_USER_KEY, userDetails) - log.info(strResponseSuccess); - return ResponseEntity.ok().body(LoginResponseDto(userDetails.username, userDetails.getProfilePictureFilename())); + log.info(strResponseSuccess) + return ResponseEntity.ok().body(LoginResponseDto(userDetails.username, userDetails.getProfilePictureFilename())) } @PostMapping("/login") fun loginUser(@RequestBody loginDto: LoginDto, session: HttpSession): ResponseEntity<*>{ - log.info("Endpoint loginUser called"); - log.debug("login Request with username: {}", loginDto.username); + log.info("Endpoint loginUser called") + log.debug("login Request with username: {}", loginDto.username) if( session.getAttribute(STR_SESSION_USER_KEY) != null ){ - log.info(strAlreadyLoggedIn); - return ResponseEntity.badRequest().body(strAlreadyLoggedIn); + log.info(strAlreadyLoggedIn) + return ResponseEntity.badRequest().body(strAlreadyLoggedIn) } - val authentication: Authentication; - try{ - authentication = authenticationManager.authenticate( - UsernamePasswordAuthenticationToken( - loginDto.username, - loginDto.password - ) + val authentication = authenticationManager.authenticate( + UsernamePasswordAuthenticationToken( + loginDto.username, + loginDto.password ) - } catch (authenticationException: AuthenticationException ){ - if( authenticationException is BadCredentialsException ){ - log.debug("Login attempt with bad credentials for username: {}", loginDto.username); - return ResponseEntity.status(401).body(4011); - } - log.error("Error during authentication", authenticationException); - return ResponseEntity.status(401).body(4010); - } + ) - SecurityContextHolder.getContext().authentication = authentication; + val userDetails: UserDetailsImpl = authentication.principal as UserDetailsImpl - val userDetails: UserDetailsImpl = authentication.principal as UserDetailsImpl; + session.setAttribute(STR_SESSION_USER_KEY, userDetails) - session.setAttribute(STR_SESSION_USER_KEY, userDetails); - - log.info(strResponseSuccess); + log.info(strResponseSuccess) return ResponseEntity.ok() .body( LoginResponseDto(userDetails.username, userDetails.getProfilePictureFilename())) } @PostMapping("/logout") fun logoutUser(session: HttpSession): ResponseEntity{ - log.info("Endpoint logoutUser called"); + log.info("Endpoint logoutUser called") - session.invalidate(); + session.invalidate() - log.info(strResponseSuccess); + log.info(strResponseSuccess) return ResponseEntity.ok() - .body("Logged out"); + .body("Logged out") } @GetMapping("/status") fun checkStatus(session: HttpSession): ResponseEntity<*>{ - log.info("Endpoint checkStatus called"); + log.info("Endpoint checkStatus called") return if( session.getAttribute(STR_SESSION_USER_KEY) != null ){ - log.info(strAlreadyLoggedIn); - val sessionUser: UserDetailsImpl = session.getAttribute(STR_SESSION_USER_KEY) as UserDetailsImpl; + log.info(strAlreadyLoggedIn) + val sessionUser: UserDetailsImpl = session.getAttribute(STR_SESSION_USER_KEY) as UserDetailsImpl ResponseEntity .ok() - .body( LoginResponseDto( sessionUser.username, sessionUser.getProfilePictureFilename() ) ); + .body( LoginResponseDto( sessionUser.username, sessionUser.getProfilePictureFilename() ) ) } else { - log.debug("No user logged in"); - log.info(strResponseSuccess); + log.debug("No user logged in") + log.info(strResponseSuccess) ResponseEntity .status(401) - .body("No user logged in"); + .body("No user logged in") } } } \ No newline at end of file diff --git a/src/main/kotlin/at/eisibaer/jbear2/endpoint/FileEndpoint.kt b/src/main/kotlin/at/eisibaer/jbear2/endpoint/FileEndpoint.kt deleted file mode 100644 index 4e6900d..0000000 --- a/src/main/kotlin/at/eisibaer/jbear2/endpoint/FileEndpoint.kt +++ /dev/null @@ -1,31 +0,0 @@ -package at.eisibaer.jbear2.endpoint - -import at.eisibaer.jbear2.exception.StorageFileNotFoundException -import at.eisibaer.jbear2.service.FileService -import at.eisibaer.jbear2.service.StorageService -import org.springframework.http.ResponseEntity -import org.springframework.stereotype.Controller -import org.springframework.web.bind.annotation.ExceptionHandler -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestParam -import org.springframework.web.multipart.MultipartFile - - -@Controller -class FileEndpoint( - val storageService: StorageService, - val fileService: FileService, -) { - - @PostMapping("/") - fun handleFileUpload( @RequestParam("files") files: List ): ResponseEntity<*> { - fileService.saveFiles(files); - - return ResponseEntity.ok().build() - } - - @ExceptionHandler(StorageFileNotFoundException::class) - fun handleStorageFileNotFound(exc: StorageFileNotFoundException?): ResponseEntity<*> { - return ResponseEntity.notFound().build() - } -} \ No newline at end of file diff --git a/src/main/kotlin/at/eisibaer/jbear2/endpoint/UserEndpoint.kt b/src/main/kotlin/at/eisibaer/jbear2/endpoint/UserEndpoint.kt index 5792ffa..af1523a 100644 --- a/src/main/kotlin/at/eisibaer/jbear2/endpoint/UserEndpoint.kt +++ b/src/main/kotlin/at/eisibaer/jbear2/endpoint/UserEndpoint.kt @@ -12,6 +12,7 @@ import org.springframework.http.ResponseEntity import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.web.bind.annotation.* +import org.springframework.web.multipart.MultipartFile @RestController @RequestMapping("/api/user") @@ -21,65 +22,65 @@ class UserEndpoint( val fileRepository: FileRepository, ) { - val log: Logger = LoggerFactory.getLogger(UserEndpoint::class.java); + val log: Logger = LoggerFactory.getLogger(UserEndpoint::class.java) @GetMapping("/test/{pathVar}") fun testEndpoint(@PathVariable pathVar: String, @RequestParam param1: String): ResponseEntity{ - log.info("test Endpoint!"); - return ResponseEntity.ok(param1); + log.info("test Endpoint!") + return ResponseEntity.ok(param1) } @GetMapping("/boards") fun getBoards(): ResponseEntity?>{ - log.info("Get all Board Endpoint"); - val userDetails = SecurityContextHolder.getContext().authentication.principal as UserDetailsImpl; - val user: User = userRepository.findUserByUsername(userDetails.username) ?: throw UsernameNotFoundException("Username ${userDetails.username} not found in DB"); + log.info("Get all Board Endpoint") + val userDetails = SecurityContextHolder.getContext().authentication.principal as UserDetailsImpl + val user: User = userRepository.findUserByUsername(userDetails.username) ?: throw UsernameNotFoundException("Username ${userDetails.username} not found in DB") - return boardService.getBoardsByUser( user ); + return boardService.getBoardsByUser( user ) } @GetMapping("/boards/{boardId}") fun getBoardById(@PathVariable boardId: Long): ResponseEntity{ - log.info("Get Board Endpoint with id {}", boardId); + log.info("Get Board Endpoint with id {}", boardId) val userDetails: UserDetailsImpl = SecurityContextHolder.getContext().authentication.principal as UserDetailsImpl - val user: User = userRepository.findUserByUsername(userDetails.username) ?: throw UsernameNotFoundException("Username ${userDetails.username} not found in DB"); + val user: User = userRepository.findUserByUsername(userDetails.username) ?: throw UsernameNotFoundException("Username ${userDetails.username} not found in DB") return boardService.getBoardByUserAndId( user, boardId) } @PostMapping("/boards") - fun saveNewBoard(@RequestBody boardDto: BoardDto): ResponseEntity<*> { - log.info("Post Board Endpoint"); + fun saveNewBoard(@RequestPart("files") files: List, @RequestPart("board") boardDto: BoardDto): ResponseEntity<*> { + log.info("Post Board Endpoint") val userDetails: UserDetailsImpl = SecurityContextHolder.getContext().authentication.principal as UserDetailsImpl - val user: User = userRepository.findUserByUsername(userDetails.username) ?: throw UsernameNotFoundException("Username ${userDetails.username} not found in DB"); + val user: User = userRepository.findUserByUsername(userDetails.username) ?: throw UsernameNotFoundException("Username ${userDetails.username} not found in DB") if( boardDto.id != null ){ - return ResponseEntity.badRequest().body(Unit); + return ResponseEntity.badRequest().body(Unit) } - return boardService.saveBoardToUser( user, boardDto ); + return boardService.saveBoardToUser( user, boardDto, files ) } @PutMapping("/boards/{boardId}") - fun saveExistingBoard(@RequestBody boardDto: BoardDto, @PathVariable boardId: Long): ResponseEntity<*> { - log.info("Put Board Endpoint with id {}", boardId); + fun saveExistingBoard(@RequestPart("files") files: List, @RequestPart("board") boardDto: BoardDto, @PathVariable boardId: Long): ResponseEntity<*> { + log.info("Put Board Endpoint with id {}", boardId) val userDetails: UserDetailsImpl = SecurityContextHolder.getContext().authentication.principal as UserDetailsImpl - val user: User = userRepository.findUserByUsername(userDetails.username) ?: throw UsernameNotFoundException("Username ${userDetails.username} not found in DB"); + val user: User = userRepository.findUserByUsername(userDetails.username) ?: throw UsernameNotFoundException("Username ${userDetails.username} not found in DB") if( boardDto.id == null || boardDto.id != boardId ){ return ResponseEntity.badRequest().body("ID is either null or does not match") } - return boardService.saveBoardToUser( user, boardDto ); + return boardService.saveBoardToUser( user, boardDto, files ) } @DeleteMapping("/boards/{boardId}") fun saveNewBoard(@PathVariable boardId: Long): ResponseEntity<*> { - log.info("Delete Board Endpoint with id {}", boardId); + log.info("Delete Board Endpoint with id {}", boardId) val userDetails: UserDetailsImpl = SecurityContextHolder.getContext().authentication.principal as UserDetailsImpl - val user: User = userRepository.findUserByUsername(userDetails.username) ?: throw UsernameNotFoundException("Username ${userDetails.username} not found in DB"); + val user: User = userRepository.findUserByUsername(userDetails.username) ?: throw UsernameNotFoundException("Username ${userDetails.username} not found in DB") - return boardService.deleteBoardOfUser( user, boardId ); + return boardService.deleteBoardOfUser( user, boardId ) } } \ No newline at end of file diff --git a/src/main/kotlin/at/eisibaer/jbear2/endpoint/ws/GameEndpoint.kt b/src/main/kotlin/at/eisibaer/jbear2/endpoint/ws/GameEndpoint.kt index 63828d5..6e3c0b9 100644 --- a/src/main/kotlin/at/eisibaer/jbear2/endpoint/ws/GameEndpoint.kt +++ b/src/main/kotlin/at/eisibaer/jbear2/endpoint/ws/GameEndpoint.kt @@ -1,9 +1,6 @@ package at.eisibaer.jbear2.endpoint.ws -import at.eisibaer.jbear2.dto.board.QuestionDto import at.eisibaer.jbear2.dto.message.* -import at.eisibaer.jbear2.exception.NoMessageException -import at.eisibaer.jbear2.model.BoardEntry import at.eisibaer.jbear2.model.Game import at.eisibaer.jbear2.model.Player import at.eisibaer.jbear2.model.User @@ -20,18 +17,13 @@ import at.eisibaer.jbear2.util.RandomString import jakarta.transaction.Transactional import org.slf4j.Logger import org.slf4j.LoggerFactory -import org.springframework.dao.DataIntegrityViolationException import org.springframework.http.ResponseEntity import org.springframework.messaging.handler.annotation.DestinationVariable -import org.springframework.messaging.handler.annotation.Header -import org.springframework.messaging.handler.annotation.Headers import org.springframework.messaging.handler.annotation.MessageExceptionHandler import org.springframework.messaging.handler.annotation.MessageMapping import org.springframework.messaging.handler.annotation.Payload -import org.springframework.messaging.handler.annotation.SendTo import org.springframework.messaging.simp.SimpMessagingTemplate import org.springframework.messaging.simp.annotation.SendToUser -import org.springframework.messaging.simp.annotation.SubscribeMapping import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.core.userdetails.UsernameNotFoundException import org.springframework.stereotype.Controller @@ -40,10 +32,8 @@ import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import java.lang.Exception -import java.security.Principal import java.util.* import kotlin.NoSuchElementException -import kotlin.collections.ArrayList @Controller class GameEndpoint( @@ -59,16 +49,16 @@ class GameEndpoint( val messagingTemplate: SimpMessagingTemplate, ) { - private val log: Logger = LoggerFactory.getLogger(GameEndpoint::class.java); + private val log: Logger = LoggerFactory.getLogger(GameEndpoint::class.java) @Transactional @GetMapping("/api/games/{id}") fun getGameByInviteCode(@PathVariable id: String): ResponseEntity<*>{ - val game: Game; + val game: Game if( id.length == INVITE_CODE_LENGTH){ game = gameRepository.findByInviteCode(id) ?: return ResponseEntity.status(404).body("Game with invide code $id not found") } else { - var uuid: UUID; + var uuid: UUID try{ uuid = UUID.fromString(id) }catch (exception: IllegalArgumentException){ @@ -76,24 +66,24 @@ class GameEndpoint( } game = gameRepository.findByUuid(uuid) ?: return ResponseEntity.status(404).body("Game with UUID $uuid not found") } - return ResponseEntity.ok().body(gameMapper.toDto(game)); + return ResponseEntity.ok().body(gameMapper.toDto(game)) } @Transactional @PostMapping("/api/games") fun hostNewGame(@RequestBody body: String?): ResponseEntity<*>{ - log.info("Start new game"); - val userDetails = SecurityContextHolder.getContext().authentication.principal as UserDetailsImpl; - val user: User = userRepository.findUserByUsername(userDetails.username) ?: throw UsernameNotFoundException("Username ${userDetails.username} not found in DB"); + log.info("Start new game") + val userDetails = SecurityContextHolder.getContext().authentication.principal as UserDetailsImpl + val user: User = userRepository.findUserByUsername(userDetails.username) ?: throw UsernameNotFoundException("Username ${userDetails.username} not found in DB") val firstAvailableBoard = boardRepository.findFirstByOwnerOrderByBoardName(user) ?: return ResponseEntity.status(404).body("No board found for user") - gameRepository.deleteAll(); + gameRepository.deleteAll() gameRepository.flush() val game = Game(RandomString.ofLength(8), UUID.randomUUID(), firstAvailableBoard, user, emptyList(), emptyList(), null ) - val savedGame = gameRepository.save(game); - return ResponseEntity.ok().body(gameMapper.toDto(savedGame)); + val savedGame = gameRepository.save(game) + return ResponseEntity.ok().body(gameMapper.toDto(savedGame)) } // @MessageMapping("/join") @@ -138,11 +128,11 @@ class GameEndpoint( @MessageMapping("/join/{id}") fun playerJoining(@Payload joinMessage: GenericMessage, @DestinationVariable id: String){ - val game: Game; + val game: Game if( id.length == INVITE_CODE_LENGTH){ game = gameRepository.findByInviteCode(id) ?: throw NoSuchElementException("Game with invide code $id not found") } else { - var uuid: UUID; + var uuid: UUID try{ uuid = UUID.fromString(id) }catch (exception: IllegalArgumentException){ @@ -150,22 +140,22 @@ class GameEndpoint( } game = gameRepository.findByUuid(uuid) ?: throw NoSuchElementException("Game with UUID $uuid not found") } - val player = Player(joinMessage.content, game); + val player = Player(joinMessage.content, game) val savedPlayer = playerRepository.save(player) - val payload = playerMapper.toDto(savedPlayer); - messagingTemplate.convertAndSend("/topic/game/${game.uuid}/joined", payload); + val payload = playerMapper.toDto(savedPlayer) + messagingTemplate.convertAndSend("/topic/game/${game.uuid}/joined", payload) } @MessageMapping("/host/game/{uuid}/start") fun startGame(@Payload startMessage: GenericMessage, @DestinationVariable uuid: UUID){ // val game = gameRepository.findByUuid(uuid) ?: throw NoSuchElementException("Game with UUID $uuid not found") - messagingTemplate.convertAndSend("/topic/game/$uuid/started", GameMessage(uuid, "The game has been started")); + messagingTemplate.convertAndSend("/topic/game/$uuid/started", GameMessage(uuid, "The game has been started")) } @MessageMapping("/host/game/{uuid}/board/select") fun selectBoard(@Payload boardMessage: GenericMessage, @DestinationVariable uuid: UUID){ - messagingTemplate.convertAndSend("/topic/game/$uuid/board/selected", GameMessage(uuid, "The board is being shown")); + messagingTemplate.convertAndSend("/topic/game/$uuid/board/selected", GameMessage(uuid, "The board is being shown")) } @MessageMapping("/host/game/{uuid}/boardentry/select") @@ -176,9 +166,9 @@ class GameEndpoint( targetGameState.boardEntryIndex, null, null, - ); + ) - messagingTemplate.convertAndSend("/topic/game/$uuid/boardentry/selected", gameState); + messagingTemplate.convertAndSend("/topic/game/$uuid/boardentry/selected", gameState) } @MessageMapping("/host/game/{uuid}/question/layer/select") @@ -189,19 +179,19 @@ class GameEndpoint( targetGameState.boardEntryIndex, targetGameState.questionIndex, targetGameState.questionLayerIndex, - ); + ) - messagingTemplate.convertAndSend("/topic/game/$uuid/questionlayer/selected", gameState); + messagingTemplate.convertAndSend("/topic/game/$uuid/questionlayer/selected", gameState) } @MessageMapping("/host/game/{uuid}/question/audio/play") fun playAudio(@Payload audioMessage: GenericMessage, @DestinationVariable uuid: UUID){ - messagingTemplate.convertAndSend("/topic/game/$uuid/question/audio/playing", GenericMessage("Playing audio")); + messagingTemplate.convertAndSend("/topic/game/$uuid/question/audio/playing", GenericMessage("Playing audio")) } @MessageMapping("/host/game/{uuid}/question/audio/stop") fun stopAudio(@Payload audioMessage: GenericMessage, @DestinationVariable uuid: UUID){ - messagingTemplate.convertAndSend("/topic/game/$uuid/question/audio/stopped", GenericMessage("Playing audio")); + messagingTemplate.convertAndSend("/topic/game/$uuid/question/audio/stopped", GenericMessage("Playing audio")) } @MessageMapping("/host/game/{uuid}/question/reveal") @@ -211,21 +201,21 @@ class GameEndpoint( gameState.boardEntryIndex == null || gameState.questionIndex == null ){ - throw InvalidPropertiesFormatException("Invalid gameSate to reveal question"); + throw InvalidPropertiesFormatException("Invalid gameSate to reveal question") } val game = gameRepository.findByUuid(uuid) ?: throw NoSuchElementException("Game with UUID $uuid not found") - val board = game.board; + val board = game.board - val question = board.categories[gameState.categoryIndex].boardEntries[gameState.boardEntryIndex].questions[gameState.questionIndex]; + val question = board.categories[gameState.categoryIndex].boardEntries[gameState.boardEntryIndex].questions[gameState.questionIndex] - val questionDto = questionMapper.toDto(question); - messagingTemplate.convertAndSend("/topic/game/$uuid/question/revealed", questionDto); + val questionDto = questionMapper.toDto(question) + messagingTemplate.convertAndSend("/topic/game/$uuid/question/revealed", questionDto) } @MessageExceptionHandler @SendToUser("/error") fun handleException(exception: Exception): GenericMessage{ log.error("Exception in GameEndpoint", exception) - return GenericMessage(exception.message ?: "Unknown error"); + return GenericMessage(exception.message ?: "Unknown error") } } \ No newline at end of file diff --git a/src/main/kotlin/at/eisibaer/jbear2/model/Answer.kt b/src/main/kotlin/at/eisibaer/jbear2/model/Answer.kt index 3ba1065..b409735 100644 --- a/src/main/kotlin/at/eisibaer/jbear2/model/Answer.kt +++ b/src/main/kotlin/at/eisibaer/jbear2/model/Answer.kt @@ -18,7 +18,7 @@ data class Answer( @OneToOne @JoinColumn(name = "fk_image_file", referencedColumnName = "id") - val image: File?, + var image: File?, @OneToOne @JoinColumn(name="fk_board_entry", referencedColumnName = "id") diff --git a/src/main/kotlin/at/eisibaer/jbear2/model/Board.kt b/src/main/kotlin/at/eisibaer/jbear2/model/Board.kt index b33bc40..51883ed 100644 --- a/src/main/kotlin/at/eisibaer/jbear2/model/Board.kt +++ b/src/main/kotlin/at/eisibaer/jbear2/model/Board.kt @@ -9,20 +9,19 @@ import jakarta.persistence.* data class Board( @OneToMany(mappedBy = "board", cascade = [CascadeType.ALL], orphanRemoval = true) - val categories: List, + val categories: List = ArrayList(), @Column(name = "board_name", nullable = false, unique = false) - val boardName: String, + var boardName: String = "New Board", @ManyToOne @JoinColumn(name="fk_owned_by", referencedColumnName = "id") - val owner: User, + var owner: User, @Column(name = "points_are_title", nullable = false, unique = false) - val pointsAreTitle: Boolean = false, + var pointsAreTitle: Boolean = false, @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator") - val id: Long? = null, - + var id: Long? = null, ) diff --git a/src/main/kotlin/at/eisibaer/jbear2/model/BoardEntry.kt b/src/main/kotlin/at/eisibaer/jbear2/model/BoardEntry.kt index 2c6b1a6..e13efff 100644 --- a/src/main/kotlin/at/eisibaer/jbear2/model/BoardEntry.kt +++ b/src/main/kotlin/at/eisibaer/jbear2/model/BoardEntry.kt @@ -9,20 +9,20 @@ import jakarta.persistence.* data class BoardEntry( @Column(name = "name", nullable = false, unique = false) - val name: String, + var name: String, @Column(name = "points", nullable = false, unique = false) - val points: Long, + var points: Long, @OneToMany(mappedBy = "boardEntry", cascade = [CascadeType.ALL], orphanRemoval = true) - val questions: List, + val questions: List = ArrayList(), @OneToOne(cascade = [CascadeType.ALL], orphanRemoval = true) - val answer: Answer, + var answer: Answer? = null, @ManyToOne @JoinColumn(name = "fk_category", referencedColumnName = "id") - var category: Category?, + var category: Category? = null, @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator") diff --git a/src/main/kotlin/at/eisibaer/jbear2/model/Category.kt b/src/main/kotlin/at/eisibaer/jbear2/model/Category.kt index 3f53e26..b6aa26a 100644 --- a/src/main/kotlin/at/eisibaer/jbear2/model/Category.kt +++ b/src/main/kotlin/at/eisibaer/jbear2/model/Category.kt @@ -9,13 +9,13 @@ import jakarta.persistence.* data class Category ( @Column(name = "name", nullable = false, unique = false) - val name: String, + var name: String, @Column(name = "description", nullable = false, unique = false) - val description: String, + var description: String, @OneToMany(mappedBy = "category", cascade = [CascadeType.ALL], orphanRemoval = true) - val boardEntries: List, + val boardEntries: List = ArrayList(), @ManyToOne @JoinColumn(name = "fk_board", referencedColumnName = "id") diff --git a/src/main/kotlin/at/eisibaer/jbear2/model/File.kt b/src/main/kotlin/at/eisibaer/jbear2/model/File.kt index 53f3bd5..dd2ec29 100644 --- a/src/main/kotlin/at/eisibaer/jbear2/model/File.kt +++ b/src/main/kotlin/at/eisibaer/jbear2/model/File.kt @@ -1,5 +1,6 @@ package at.eisibaer.jbear2.model +import at.eisibaer.jbear2.model.enums.FileType import jakarta.persistence.* import java.util.UUID @@ -34,10 +35,4 @@ data class File ( @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator") val id: Long? = null, -) - -enum class FileType{ - IMAGE, - AUDIO, - PROFILE_PICTURE, -} \ No newline at end of file +) \ No newline at end of file diff --git a/src/main/kotlin/at/eisibaer/jbear2/model/Question.kt b/src/main/kotlin/at/eisibaer/jbear2/model/Question.kt index acc14c4..2abfe9b 100644 --- a/src/main/kotlin/at/eisibaer/jbear2/model/Question.kt +++ b/src/main/kotlin/at/eisibaer/jbear2/model/Question.kt @@ -12,18 +12,18 @@ import jakarta.persistence.* data class Question( @Column(name = "text", nullable = false, unique = false) - val text: String, + var text: String, @Enumerated @Column(name = "question_type", nullable = false, unique = false) - val questionType: QuestionType = QuestionType.TEXT, + var questionType: QuestionType = QuestionType.TEXT, @Column(name = "font_scaling", nullable = false, unique = false) - val fontScaling: Int, + var fontScaling: Int, @OneToOne @JoinColumn(name = "fk_image", referencedColumnName = "id") - val image: File?, + var image: File?, @ManyToOne @JoinColumn(name = "fk_board_entry", referencedColumnName = "id") diff --git a/src/main/kotlin/at/eisibaer/jbear2/model/enums/FileType.kt b/src/main/kotlin/at/eisibaer/jbear2/model/enums/FileType.kt new file mode 100644 index 0000000..6784a36 --- /dev/null +++ b/src/main/kotlin/at/eisibaer/jbear2/model/enums/FileType.kt @@ -0,0 +1,6 @@ +package at.eisibaer.jbear2.model.enums + +enum class FileType{ + IMAGE, + AUDIO, +} \ No newline at end of file diff --git a/src/main/kotlin/at/eisibaer/jbear2/repository/AnswerRepository.kt b/src/main/kotlin/at/eisibaer/jbear2/repository/AnswerRepository.kt new file mode 100644 index 0000000..3429275 --- /dev/null +++ b/src/main/kotlin/at/eisibaer/jbear2/repository/AnswerRepository.kt @@ -0,0 +1,6 @@ +package at.eisibaer.jbear2.repository + +import at.eisibaer.jbear2.model.Answer +import org.springframework.data.jpa.repository.JpaRepository + +interface AnswerRepository : JpaRepository \ No newline at end of file diff --git a/src/main/kotlin/at/eisibaer/jbear2/repository/QuestionRepository.kt b/src/main/kotlin/at/eisibaer/jbear2/repository/QuestionRepository.kt new file mode 100644 index 0000000..bc46af9 --- /dev/null +++ b/src/main/kotlin/at/eisibaer/jbear2/repository/QuestionRepository.kt @@ -0,0 +1,6 @@ +package at.eisibaer.jbear2.repository + +import at.eisibaer.jbear2.model.Question +import org.springframework.data.jpa.repository.JpaRepository + +interface QuestionRepository : JpaRepository \ No newline at end of file diff --git a/src/main/kotlin/at/eisibaer/jbear2/security/SecurityConfiguration.kt b/src/main/kotlin/at/eisibaer/jbear2/security/SecurityConfiguration.kt index 050ea30..0f47a50 100644 --- a/src/main/kotlin/at/eisibaer/jbear2/security/SecurityConfiguration.kt +++ b/src/main/kotlin/at/eisibaer/jbear2/security/SecurityConfiguration.kt @@ -1,6 +1,7 @@ package at.eisibaer.jbear2.security import at.eisibaer.jbear2.config.ApplicationProperties +import at.eisibaer.jbear2.repository.UserRepository import jakarta.servlet.FilterChain import jakarta.servlet.ServletException import jakarta.servlet.http.HttpServletRequest @@ -10,8 +11,8 @@ import org.slf4j.LoggerFactory import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.security.authentication.AuthenticationManager +import org.springframework.security.authentication.ProviderManager import org.springframework.security.authentication.dao.DaoAuthenticationProvider -import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.core.userdetails.UserDetailsService @@ -29,12 +30,12 @@ import java.util.function.Supplier @Configuration @EnableMethodSecurity class SecurityConfiguration( - private val userDetailService: UserDetailsService, + private val userRepository: UserRepository, private val unauthorizedHandler: AuthFilter, private val applicationProperties: ApplicationProperties ) { - final val log: Logger = LoggerFactory.getLogger(SecurityConfiguration::class.java); + final val log: Logger = LoggerFactory.getLogger(SecurityConfiguration::class.java) @Bean fun securityFilterChain(httpSecurity: HttpSecurity): SecurityFilterChain { @@ -44,14 +45,13 @@ class SecurityConfiguration( .requestMatchers("/api/user/**").authenticated() .requestMatchers("/**").permitAll() } - .authenticationProvider(authenticationProvider()) .addFilterBefore(unauthorizedHandler, UsernamePasswordAuthenticationFilter::class.java) .build() } private fun addCsrfConfig(httpSecurity: HttpSecurity): HttpSecurity{ if( applicationProperties.test ){ - httpSecurity.csrf{ config -> config.disable()}; + httpSecurity.csrf{ config -> config.disable()} } else { httpSecurity.csrf { config -> config @@ -60,7 +60,7 @@ class SecurityConfiguration( } .addFilterAfter(CsrfCookieFilter(), BasicAuthenticationFilter::class.java) } - return httpSecurity; + return httpSecurity } class SpaCsrfTokenRequestHandler : CsrfTokenRequestAttributeHandler() { @@ -107,21 +107,18 @@ class SecurityConfiguration( @Bean fun passwordEncoder() : PasswordEncoder { - return Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8(); + return Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8() } @Bean - fun authenticationProvider(): DaoAuthenticationProvider{ - val authProvider: DaoAuthenticationProvider = DaoAuthenticationProvider() - - authProvider.setUserDetailsService(userDetailService) - authProvider.setPasswordEncoder(passwordEncoder()); - - return authProvider; + fun authenticationProvider(): AuthenticationManager { + val authProvider = DaoAuthenticationProvider(userDetailService()) + authProvider.setPasswordEncoder(passwordEncoder()) + return ProviderManager(authProvider) } @Bean - fun authenticationManager(authConfig: AuthenticationConfiguration): AuthenticationManager{ - return authConfig.authenticationManager; + fun userDetailService(): UserDetailsService { + return UserDetailServiceImpl(userRepository) } } \ No newline at end of file diff --git a/src/main/kotlin/at/eisibaer/jbear2/security/UserDetailServiceImpl.kt b/src/main/kotlin/at/eisibaer/jbear2/security/UserDetailServiceImpl.kt index 823f55c..94a0fd1 100644 --- a/src/main/kotlin/at/eisibaer/jbear2/security/UserDetailServiceImpl.kt +++ b/src/main/kotlin/at/eisibaer/jbear2/security/UserDetailServiceImpl.kt @@ -16,17 +16,17 @@ class UserDetailServiceImpl( ): UserDetailsService { override fun loadUserByUsername(username: String?): UserDetailsImpl { - val user: User? = userRepository.findUserByUsername( username ?: "" ) - - if( user == null ){ - throw UsernameNotFoundException("User not found by username \"$username\""); + if( username == null ){ + throw UsernameNotFoundException("User not found by username \"$username\"") } + val user: User = userRepository.findUserByUsername( username ) + ?: throw UsernameNotFoundException("User not found by username \"$username\"") return UserDetailsImpl( user.id!!, user.username, user.password, user.profilePicture?.filename, - ); + ) } } \ No newline at end of file diff --git a/src/main/kotlin/at/eisibaer/jbear2/service/BoardService.kt b/src/main/kotlin/at/eisibaer/jbear2/service/BoardService.kt index 8650365..7409540 100644 --- a/src/main/kotlin/at/eisibaer/jbear2/service/BoardService.kt +++ b/src/main/kotlin/at/eisibaer/jbear2/service/BoardService.kt @@ -3,6 +3,7 @@ package at.eisibaer.jbear2.service import at.eisibaer.jbear2.dto.board.BoardDto import at.eisibaer.jbear2.model.User import org.springframework.http.ResponseEntity +import org.springframework.web.multipart.MultipartFile interface BoardService { @@ -10,6 +11,6 @@ interface BoardService { fun getBoardByUserAndId( user: User, boardId: Long ): ResponseEntity - fun saveBoardToUser( user: User, boardDto: BoardDto ): ResponseEntity + fun saveBoardToUser( user: User, boardDto: BoardDto, files: List ): ResponseEntity fun deleteBoardOfUser( user: User, boardId: Long ): ResponseEntity } \ No newline at end of file diff --git a/src/main/kotlin/at/eisibaer/jbear2/service/BoardServiceImpl.kt b/src/main/kotlin/at/eisibaer/jbear2/service/BoardServiceImpl.kt index 271f569..f0029aa 100644 --- a/src/main/kotlin/at/eisibaer/jbear2/service/BoardServiceImpl.kt +++ b/src/main/kotlin/at/eisibaer/jbear2/service/BoardServiceImpl.kt @@ -9,54 +9,92 @@ import at.eisibaer.jbear2.repository.UserRepository import at.eisibaer.jbear2.service.mapper.BoardMapper import org.slf4j.Logger import org.slf4j.LoggerFactory +import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import org.springframework.web.multipart.MultipartFile @Service class BoardServiceImpl ( + private val fileService: FileService, private val boardRepository: BoardRepository, private val userRepository: UserRepository, private val fileRepository: FileRepository, private val boardMapper: BoardMapper, ) : BoardService { - val log: Logger = LoggerFactory.getLogger(BoardServiceImpl::class.java); + val log: Logger = LoggerFactory.getLogger(BoardServiceImpl::class.java) @Transactional override fun getBoardsByUser(user: User): ResponseEntity?> { val boards = boardRepository.findAllByOwner(user) - return ResponseEntity.ok(boardMapper.toDto(boards)); + return ResponseEntity.ok(boardMapper.toDto(boards)) } @Transactional override fun getBoardByUserAndId(user: User, boardId: Long): ResponseEntity { - val board = boardRepository.findByIdAndOwner(boardId, user) ?: return ResponseEntity.status(404).build(); + val board = boardRepository.findByIdAndOwner(boardId, user) ?: return ResponseEntity.status(404).build() return ResponseEntity.ok(boardMapper.toDto(board)) } @Transactional - override fun saveBoardToUser( user: User, boardDto: BoardDto): ResponseEntity { - val board: Board = boardMapper.toEntity(boardDto, user, userRepository, fileRepository); + override fun saveBoardToUser( user: User, boardDto: BoardDto, files: List): ResponseEntity { + val board = if( boardDto.id == null ){ + Board(owner = user) + } else { + boardRepository.findByIdAndOwner(boardDto.id, user) ?: throw NoSuchElementException("Board not found for user " + user.username) + } + boardMapper.mapToEntity(board, boardDto, user, userRepository, fileRepository) - val savedBoard: Board; + val savedBoard: Board try { - savedBoard = boardRepository.save(board); + savedBoard = boardRepository.save(board) } catch (ex: Exception) { - log.error(ex.message, ex); - return ResponseEntity.badRequest().build(); + log.error(ex.message, ex) + return ResponseEntity.badRequest().build() + } + + try{ + fileService.saveFiles(files, user, savedBoard) + } catch (ex: Exception) { + log.error(ex.message, ex) + return ResponseEntity.badRequest().build() } return ResponseEntity.ok(boardMapper.toDto(savedBoard)) } override fun deleteBoardOfUser(user: User, boardId: Long): ResponseEntity { - boardRepository.deleteById( boardId ); + val board = boardRepository.findByIdAndOwner(boardId, user) ?: return ResponseEntity.status(HttpStatus.NOT_FOUND).build() + + + boardRepository.deleteById( boardId ) + //TODO Delete Images - return ResponseEntity.ok().build(); + return ResponseEntity.ok().build() + } + + private fun deleteFilesOfBoard(board: Board) { + try { + for (category in board.categories) { + for (boardEntry in category.boardEntries) { + if (boardEntry.answer != null && boardEntry.answer!!.image != null) { + fileService.deleteFile(boardEntry.answer!!.image!!) + } + for (question in boardEntry.questions) { + if (question.image != null) { + fileService.deleteFile(question.image!!) + } + } + } + } + } catch (exception: Exception) { + log.error("Error occured",exception) //TODO + } } } \ No newline at end of file diff --git a/src/main/kotlin/at/eisibaer/jbear2/service/FileService.kt b/src/main/kotlin/at/eisibaer/jbear2/service/FileService.kt index 6d88461..7be4c70 100644 --- a/src/main/kotlin/at/eisibaer/jbear2/service/FileService.kt +++ b/src/main/kotlin/at/eisibaer/jbear2/service/FileService.kt @@ -1,18 +1,20 @@ package at.eisibaer.jbear2.service +import at.eisibaer.jbear2.model.Board import at.eisibaer.jbear2.model.File +import at.eisibaer.jbear2.model.User import org.springframework.core.io.Resource import org.springframework.web.multipart.MultipartFile import java.util.UUID interface FileService { - fun saveFiles(files: List): List; - fun saveFile(file: MultipartFile): File; + fun saveFiles(files: List, owner: User, board: Board) + fun saveFile(file: MultipartFile, owner: User, board: Board): File - fun getFile(file: File): Resource; - fun getFile(fileUUID: UUID): Resource; + fun getFile(file: File): Resource + fun getFile(fileUUID: UUID): Resource - fun deleteFile(file: File): Resource; - fun deleteFile(fileUUID: UUID): Resource; + fun deleteFile(file: File): Resource + fun deleteFile(fileUUID: UUID): Resource } diff --git a/src/main/kotlin/at/eisibaer/jbear2/service/FileServiceImpl.kt b/src/main/kotlin/at/eisibaer/jbear2/service/FileServiceImpl.kt index 6b3f9d7..873f4c6 100644 --- a/src/main/kotlin/at/eisibaer/jbear2/service/FileServiceImpl.kt +++ b/src/main/kotlin/at/eisibaer/jbear2/service/FileServiceImpl.kt @@ -1,36 +1,96 @@ package at.eisibaer.jbear2.service -import at.eisibaer.jbear2.model.File +import at.eisibaer.jbear2.model.* +import at.eisibaer.jbear2.model.enums.FileType +import at.eisibaer.jbear2.repository.AnswerRepository import at.eisibaer.jbear2.repository.FileRepository +import at.eisibaer.jbear2.repository.QuestionRepository +import lombok.extern.slf4j.Slf4j +import org.apache.tomcat.util.http.fileupload.InvalidFileNameException +import org.slf4j.Logger +import org.slf4j.LoggerFactory import org.springframework.core.io.Resource import org.springframework.stereotype.Service +import org.springframework.util.InvalidMimeTypeException import org.springframework.web.multipart.MultipartFile -import java.nio.file.Files +import java.security.MessageDigest import java.util.* +@Slf4j @Service -class FileServiceImpl( +class FileServiceImpl ( val fileRepository: FileRepository, + val answerRepository: AnswerRepository, + val questionRepository: QuestionRepository, + val storageService: StorageService, ) : FileService { - override fun saveFiles(files: List): List { + val log: Logger = LoggerFactory.getLogger(FileServiceImpl::class.java) + + override fun saveFiles(files: List, owner: User, board: Board) { + val digest: MessageDigest = MessageDigest.getInstance("SHA-256") + for(file in files) { + if( file.originalFilename == null ){ + throw InvalidFileNameException(file.originalFilename, "No filename given") + } + val associatedEntries = file.originalFilename!!.split("-") + if( associatedEntries.size != 3) { + throw InvalidFileNameException(file.name, "Invalid index name") + } + val boardEntry = board.categories[associatedEntries[0].toInt()].boardEntries[associatedEntries[1].toInt()] + var question: Question? = null + var answer: Answer? = null + if(associatedEntries[2] == "answer"){ + answer = boardEntry.answer + } else { + question = boardEntry.questions[associatedEntries[2].toInt()] + } + + val uuid = UUID.randomUUID() + val filename = uuid.toString() + getExtension(file) + + storageService.storeFile(file, filename) + log.debug("File stored") + + val newFile = File( + uuid, + filename, + Base64.getEncoder().encodeToString(digest.digest(file.bytes)), + owner, + if (file.contentType.orEmpty().startsWith("image")) FileType.IMAGE else FileType.AUDIO, + question, + answer, + ) + val savedFile: File = fileRepository.save(newFile) + if(associatedEntries[2] == "answer"){ + if( answer != null ){ + if( savedFile.fileType == FileType.IMAGE ) { + answer.image = savedFile + } + answerRepository.save(answer) + } + } else if( question != null ){ + if( savedFile.fileType == FileType.IMAGE ) { + question.image = savedFile + } + questionRepository.save(question) + } + } + return + } + + override fun saveFile(file: MultipartFile, owner: User, board: Board): File { TODO("Not yet implemented") } - override fun saveFile(file: MultipartFile): File { - TODO("Not yet implemented") - } - - private fun saveMultipartFile(file: MultipartFile): File{ - val uuid = UUID.randomUUID(); - val filename = file.originalFilename; - var hash: String; -// val file = File( -// UUID.randomUUID(), -// uuidextension, -// hash, -// ) - TODO(); + private fun getExtension(file: MultipartFile): String { + return when (file.contentType) { + "image/jpeg" -> ".jpg" + "image/png" -> ".png" + "image/avif" -> ".avif" + "image/webp" -> ".webp" + else -> throw InvalidMimeTypeException(file.contentType ?: "unknown", "MIME Type not accepted") + } } override fun getFile(file: File): Resource { @@ -42,6 +102,7 @@ class FileServiceImpl( } override fun deleteFile(file: File): Resource { + TODO("Not yet implemented") } diff --git a/src/main/kotlin/at/eisibaer/jbear2/service/FileSystemStorageService.kt b/src/main/kotlin/at/eisibaer/jbear2/service/FileSystemStorageService.kt index 06b33bd..6a32cc7 100644 --- a/src/main/kotlin/at/eisibaer/jbear2/service/FileSystemStorageService.kt +++ b/src/main/kotlin/at/eisibaer/jbear2/service/FileSystemStorageService.kt @@ -4,11 +4,11 @@ import at.eisibaer.jbear2.config.ApplicationProperties import at.eisibaer.jbear2.exception.StorageException import at.eisibaer.jbear2.exception.StorageFileNotFoundException import jakarta.annotation.PostConstruct +import org.slf4j.Logger +import org.slf4j.LoggerFactory import org.springframework.core.io.Resource import org.springframework.core.io.UrlResource import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Propagation -import org.springframework.transaction.annotation.Transactional import org.springframework.util.FileSystemUtils import org.springframework.web.multipart.MultipartFile import java.io.IOException @@ -20,29 +20,30 @@ import java.nio.file.StandardCopyOption @Service -@Transactional(propagation = Propagation.NEVER) class FileSystemStorageService( val applicationProperties: ApplicationProperties, ) : StorageService { + val log: Logger = LoggerFactory.getLogger(FileSystemStorageService::class.java) + lateinit var rootLocation: Path @PostConstruct fun init(){ - val location = applicationProperties.storage.fs.location; + val location = applicationProperties.storage.fs.location if( location.trim().isEmpty() ){ throw StorageException("File upload location can not be Empty", null) } - rootLocation = Paths.get(location); + rootLocation = Paths.get(location) } - override fun storeFile(file: MultipartFile) { - storeMultipartFile(file); + override fun storeFile(file: MultipartFile, filename: String) { + storeMultipartFile(file, filename) } - override fun storeFiles(files: List) { + override fun storeFiles(files: List>) { for( file in files ){ - storeMultipartFile(file) + storeMultipartFile(file.first, file.second) } } @@ -64,16 +65,16 @@ class FileSystemStorageService( FileSystemUtils.deleteRecursively(rootLocation.resolve(filename)) } - private fun storeMultipartFile(file: MultipartFile){ + private fun storeMultipartFile(file: MultipartFile, filename: String){ try { if (file.isEmpty) { throw StorageException("Failed to store empty file.", null) } val destinationFile: Path = rootLocation - .resolve(Paths.get(file.originalFilename)) + .resolve(Paths.get(filename)) .normalize() .toAbsolutePath() - if (destinationFile.getParent() != rootLocation.toAbsolutePath()) { + if (destinationFile.parent != rootLocation.toAbsolutePath()) { // This is a security check throw StorageException("Cannot store file outside current directory.", null) } diff --git a/src/main/kotlin/at/eisibaer/jbear2/service/StorageService.kt b/src/main/kotlin/at/eisibaer/jbear2/service/StorageService.kt index eb654d9..ad36a07 100644 --- a/src/main/kotlin/at/eisibaer/jbear2/service/StorageService.kt +++ b/src/main/kotlin/at/eisibaer/jbear2/service/StorageService.kt @@ -5,9 +5,9 @@ import org.springframework.web.multipart.MultipartFile interface StorageService { - fun storeFile(file: MultipartFile) + fun storeFile(file: MultipartFile, filename: String) - fun storeFiles(files: List) + fun storeFiles(files: List>) fun getFile(filename: String): Resource diff --git a/src/main/kotlin/at/eisibaer/jbear2/service/mapper/BoardEntryMapper.kt b/src/main/kotlin/at/eisibaer/jbear2/service/mapper/BoardEntryMapper.kt index 2ca01b6..5a5ce6e 100644 --- a/src/main/kotlin/at/eisibaer/jbear2/service/mapper/BoardEntryMapper.kt +++ b/src/main/kotlin/at/eisibaer/jbear2/service/mapper/BoardEntryMapper.kt @@ -13,11 +13,13 @@ import java.util.* @Mapper(uses = [QuestionMapper::class,AnswerMapper::class]) abstract class BoardEntryMapper { - abstract fun toDto(e: BoardEntry): BoardEntryDto; - abstract fun toDto(e: List): List; + abstract fun toDto(e: BoardEntry): BoardEntryDto + abstract fun toDto(e: List): List - abstract fun toEntity(d: BoardEntryDto, @Context fileRepository: FileRepository): BoardEntry; - abstract fun toEntity(d: List, @Context fileRepository: FileRepository): List; + abstract fun toEntity(d: BoardEntryDto, @Context fileRepository: FileRepository): BoardEntry + abstract fun toEntity(d: List, @Context fileRepository: FileRepository): List + + abstract fun mapToEntity(d: BoardEntryDto, @MappingTarget e: BoardEntry, @Context fileRepository: FileRepository) fun map(file: File): String{ return file.uuid.toString() diff --git a/src/main/kotlin/at/eisibaer/jbear2/service/mapper/BoardMapper.kt b/src/main/kotlin/at/eisibaer/jbear2/service/mapper/BoardMapper.kt index 0154bbd..9fc1b49 100644 --- a/src/main/kotlin/at/eisibaer/jbear2/service/mapper/BoardMapper.kt +++ b/src/main/kotlin/at/eisibaer/jbear2/service/mapper/BoardMapper.kt @@ -7,16 +7,28 @@ import at.eisibaer.jbear2.repository.FileRepository import at.eisibaer.jbear2.repository.UserRepository import org.mapstruct.* -@Mapper(uses = [UserMapper::class,QuestionMapper::class,AnswerMapper::class,BoardEntryMapper::class,CategoryMapper::class]) +@Mapper(uses = [QuestionMapper::class,AnswerMapper::class,BoardEntryMapper::class,CategoryMapper::class]) abstract class BoardMapper { - abstract fun toDto(e: Board): BoardDto; - abstract fun toDto(e: List): List; + abstract fun toDto(e: Board): BoardDto + abstract fun toDto(e: List): List - @Mapping(target = "owner", source = "owner") - @Mapping(target = "id", source = "d.id") - abstract fun toEntity(d: BoardDto, owner: User, @Context userRepository: UserRepository, @Context fileRepository: FileRepository): Board; - abstract fun toEntity(d: List, @Context owner: User, @Context userRepository: UserRepository, @Context fileRepository: FileRepository): List; + @Mappings(value = [ + Mapping(target = "owner", source = "ownerUser"), + Mapping(target = "id", source = "d.id"), + ]) + abstract fun toEntity(d: BoardDto, ownerUser: User, @Context userRepository: UserRepository, @Context fileRepository: FileRepository): Board + abstract fun toEntity(d: List, ownerUser: User, @Context userRepository: UserRepository, @Context fileRepository: FileRepository): ArrayList + + @Mappings(value = [ + Mapping(target = "owner", source = "ownerUser"), + Mapping(target = "id", source = "d.id"), + ]) + abstract fun mapToEntity(@MappingTarget board: Board, d: BoardDto, ownerUser: User, @Context userRepository: UserRepository, @Context fileRepository: FileRepository): Board + + fun map(owner: User): String{ + return owner.username + } @AfterMapping fun addBoardToCategory(source: BoardDto, @MappingTarget target: Board ): Board{ diff --git a/src/main/kotlin/at/eisibaer/jbear2/service/mapper/CategoryMapper.kt b/src/main/kotlin/at/eisibaer/jbear2/service/mapper/CategoryMapper.kt index fa86e64..4b3643a 100644 --- a/src/main/kotlin/at/eisibaer/jbear2/service/mapper/CategoryMapper.kt +++ b/src/main/kotlin/at/eisibaer/jbear2/service/mapper/CategoryMapper.kt @@ -13,11 +13,11 @@ import java.util.* @Mapper(uses = [QuestionMapper::class,AnswerMapper::class,BoardEntryMapper::class]) abstract class CategoryMapper { - abstract fun toDto(e: Category): CategoryDto; - abstract fun toDto(e: List): List; + abstract fun toDto(e: Category): CategoryDto + abstract fun toDto(e: List): List - abstract fun toEntity(d: CategoryDto, @Context fileRepository: FileRepository): Category; - abstract fun toEntity(d: List, @Context fileRepository: FileRepository): List; + abstract fun toEntity(d: CategoryDto, @Context fileRepository: FileRepository, @Context boardEntryMapper: BoardEntryMapper): Category + abstract fun toEntity(d: List, @Context fileRepository: FileRepository, @Context boardEntryMapper: BoardEntryMapper): List fun map(file: File): String{ return file.uuid.toString() diff --git a/src/main/kotlin/at/eisibaer/jbear2/service/mapper/GameMapper.kt b/src/main/kotlin/at/eisibaer/jbear2/service/mapper/GameMapper.kt index ed40ef4..04ec498 100644 --- a/src/main/kotlin/at/eisibaer/jbear2/service/mapper/GameMapper.kt +++ b/src/main/kotlin/at/eisibaer/jbear2/service/mapper/GameMapper.kt @@ -1,20 +1,8 @@ package at.eisibaer.jbear2.service.mapper import at.eisibaer.jbear2.dto.game.GameDto -import at.eisibaer.jbear2.model.BoardEntry import at.eisibaer.jbear2.model.Game import org.mapstruct.Mapper -@Mapper -interface GameMapper : EntityMapper { -// -// fun map(boardEntry: BoardEntry): Long{ -// return boardEntry.id!! -// } -// fun map(boardEntries: List): List{ -// return boardEntries.map { it.id!! } -// } -// fun map(ids: List): List{ -// return emptyList() -// } -} +@Mapper(uses = [PlayerMapper::class]) +interface GameMapper : EntityMapper diff --git a/src/main/kotlin/at/eisibaer/jbear2/service/mapper/UserMapper.kt b/src/main/kotlin/at/eisibaer/jbear2/service/mapper/UserMapper.kt deleted file mode 100644 index 7cb5739..0000000 --- a/src/main/kotlin/at/eisibaer/jbear2/service/mapper/UserMapper.kt +++ /dev/null @@ -1,29 +0,0 @@ -package at.eisibaer.jbear2.service.mapper - -import at.eisibaer.jbear2.dto.user.UserDto -import at.eisibaer.jbear2.model.User -import at.eisibaer.jbear2.repository.FileRepository -import at.eisibaer.jbear2.repository.UserRepository -import org.mapstruct.Context -import org.mapstruct.Mapper -import org.springframework.security.core.userdetails.UsernameNotFoundException - -@Mapper(uses = [ImageMapper::class, UserMapper::class]) -interface UserMapper{ - - fun toDto(e: User): UserDto; - fun toDto(e: List): List; - - fun toEntity(d: UserDto, @Context userRepository: UserRepository, @Context fileRepository: FileRepository): User; - fun toEntity(d: List, @Context userRepository: UserRepository, @Context fileRepository: FileRepository): List; - - fun map(e: User): String{ - return e.username; - } - fun map(d: String, @Context userRepository: UserRepository): User{ - val user = userRepository.findUserByUsername(d) - ?: throw UsernameNotFoundException("User not found by username \"$d\"") - return user; - } - -} \ No newline at end of file diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml index bfa1591..5c0b727 100644 --- a/src/main/resources/config/application.yml +++ b/src/main/resources/config/application.yml @@ -11,14 +11,14 @@ spring: dialect: org.hibernate.dialect.PostgreSQLDialect default-schema: jeobeardy-app open-in-view: false - docker: - compose: - lifecycle-management: start-only servlet: multipart: - max-file-size: 5MB - max-request-size: 50MB -# session: + max-file-size: 1MB + max-request-size: 40MB + session: + store-type: jdbc + jdbc: + initialize-schema: always # redis: # flush-mode: on_save # namespace: spring:session diff --git a/src/main/webapp/src/assets/scss/customized_bootstrap.scss b/src/main/webapp/src/assets/scss/customized_bootstrap.scss index 8cf306a..f226209 100644 --- a/src/main/webapp/src/assets/scss/customized_bootstrap.scss +++ b/src/main/webapp/src/assets/scss/customized_bootstrap.scss @@ -48,6 +48,8 @@ $dropdown-link-hover-bg: $dark-accented; $modal-fade-transform: scale(.75); $breadcrumb-divider: quote(">"); +$border-radius: 0; + // 3. Include remainder of required Bootstrap stylesheets (including any separate color mode stylesheets) @import "bootstrap/scss/variables"; @import "bootstrap/scss/variables-dark"; @@ -55,9 +57,6 @@ $breadcrumb-divider: quote(">"); $form-file-button-bg: var(--#{$prefix}secondary-bg); $form-file-button-hover-bg: var(--#{$prefix}tertiary-bg); -// $btn-border-radius: 0; -// $card-border-radius: 0; - /* Bootstrap Color Map adjustments */ $custom-colors: ( "gray": $gray-500, diff --git a/src/main/webapp/src/components/blocks/BoardEntryView.vue b/src/main/webapp/src/components/blocks/BoardEntryView.vue index 734bbb3..45f2254 100644 --- a/src/main/webapp/src/components/blocks/BoardEntryView.vue +++ b/src/main/webapp/src/components/blocks/BoardEntryView.vue @@ -1,11 +1,9 @@ diff --git a/src/main/webapp/src/components/pages/CreatePage.vue b/src/main/webapp/src/components/pages/CreatePage.vue index a77975f..424dca1 100644 --- a/src/main/webapp/src/components/pages/CreatePage.vue +++ b/src/main/webapp/src/components/pages/CreatePage.vue @@ -11,6 +11,7 @@ import CreatePanel from '@/components/blocks/CreatePanel.vue'; import BoardEntryView from '@/components/blocks/BoardEntryView.vue'; import { userService } from '@/services/UserService'; import { useRoute, useRouter } from 'vue-router'; +import { useFileStore } from '@/stores/FileStore'; const navbar = inject | null>>(navbarKey); const navbarHeight = computed(() => { @@ -65,14 +66,20 @@ function hideAnswer() { isAnswerShown.value = false; } +const fileStore = useFileStore(); const savingBoardInProgress = ref(false); function saveBoard() { savingBoardInProgress.value = true; + const formData: FormData = new FormData(); + formData.set('board', new Blob([JSON.stringify(board.value)], { 'type': 'application/json' } )); + for( const file of fileStore.files ){ + formData.append('files', file.data, `${file.category}-${file.boardEntry}-${file.answer ? 'answer' : file.question!}`); + } let savePromise; if( board.value.id ){ - savePromise = userService.updateBoard(board.value); + savePromise = userService.updateBoard(board.value, formData); } else { - savePromise = userService.saveNewBoard(board.value); + savePromise = userService.saveNewBoard(formData); } savePromise .then((savedBoard) => { diff --git a/src/main/webapp/src/services/UserService.ts b/src/main/webapp/src/services/UserService.ts index 6b3f47c..9cea3ab 100644 --- a/src/main/webapp/src/services/UserService.ts +++ b/src/main/webapp/src/services/UserService.ts @@ -48,10 +48,10 @@ class UserService { }); }); } - saveNewBoard(board: Board): Promise { + saveNewBoard(formData: FormData): Promise { return new Promise((resolve, reject) => { axios - .post(`${ENV.API_BASE_URL}/user/boards`, board, { + .post(`${ENV.API_BASE_URL}/user/boards`, formData, { withCredentials: true, }) .then((response) => { @@ -62,13 +62,13 @@ class UserService { }); }); } - updateBoard(board: Board): Promise { + updateBoard(board: Board, formData: FormData): Promise { if( board.id === undefined ){ throw new Error("New board cant be updated"); } return new Promise((resolve, reject) => { axios - .put(`${ENV.API_BASE_URL}/user/boards/${board.id}`, board, { + .put(`${ENV.API_BASE_URL}/user/boards/${board.id}`, formData, { withCredentials: true, }) .then((response) => { diff --git a/src/main/webapp/src/services/UtilService.ts b/src/main/webapp/src/services/UtilService.ts index d4b6287..3c37e15 100644 --- a/src/main/webapp/src/services/UtilService.ts +++ b/src/main/webapp/src/services/UtilService.ts @@ -2,4 +2,30 @@ import type { InjectionKey, Ref } from 'vue'; import type NavBar from '@/components/blocks/NavBar.vue'; export const infoModalShowFnKey = Symbol() as InjectionKey; -export const navbarKey = Symbol() as InjectionKey | undefined>>; \ No newline at end of file +export const navbarKey = Symbol() as InjectionKey | undefined>>; + +export interface FileAssociation { + category: number, + boardEntry: number, + answer: boolean, + question?: number, +} +export interface FileAssociationWFile extends FileAssociation { + data: File +} + +export function getFileKeyForQuestion(categoryIndex: number, boardEntryIndex: number, questionIndex: number): FileAssociation { + return { + category: categoryIndex, + boardEntry: boardEntryIndex, + answer: false, + question: questionIndex, + }; +} +export function getFileKeyForAnswer(categoryIndex: number, boardEntryIndex: number): FileAssociation { + return { + category: categoryIndex, + boardEntry: boardEntryIndex, + answer: true, + }; +} \ No newline at end of file diff --git a/src/main/webapp/src/stores/FileStore.ts b/src/main/webapp/src/stores/FileStore.ts new file mode 100644 index 0000000..566940c --- /dev/null +++ b/src/main/webapp/src/stores/FileStore.ts @@ -0,0 +1,45 @@ +import { ref } from 'vue'; +import { defineStore } from 'pinia'; +import type { FileAssociation, FileAssociationWFile } from '@/services/UtilService'; + +export const useFileStore = defineStore('file', () => { + const files = ref>([]); + + function addFile(file: File, fileAssociation: FileAssociation) { + const fileWData = { + ...fileAssociation, + data: file, + }; + const idx = files.value.findIndex(x => + x.category === fileAssociation.category && + x.boardEntry === fileAssociation.boardEntry && + x.answer === fileAssociation.answer && + x.question === x.question + ); + if( idx === -1 ){ + files.value.splice(idx, 1, fileWData); + } else { + files.value.push(fileWData); + } + } + function deleteFile(fileAssociation: FileAssociation) { + const idxToDel = files.value.findIndex(x => + x.category === fileAssociation.category && + x.boardEntry === fileAssociation.boardEntry && + x.answer === fileAssociation.answer && + x.question === x.question + ); + if(idxToDel !== -1){ + files.value.splice(idxToDel, 1); + } + } + + return { + // state + files, + + // functions + addFile, + deleteFile, + } +}); \ No newline at end of file diff --git a/src/main/webapp/src/stores/GameStore.ts b/src/main/webapp/src/stores/GameStore.ts index 6c31148..e4d6fba 100644 --- a/src/main/webapp/src/stores/GameStore.ts +++ b/src/main/webapp/src/stores/GameStore.ts @@ -5,7 +5,11 @@ import type { Board } from '@/models/board/Board'; import { userService } from '@/services/UserService'; import { Player } from '@/models/game/Player'; import { Client as StompClient } from '@stomp/stompjs'; -import { GAME_STATUS_CONST, gameService, SESSION_GAME_KEY } from '@/services/GameService'; +import { + GAME_STATUS_CONST, + gameService, + SESSION_GAME_KEY, +} from '@/services/GameService'; import type { Game } from '@/models/game/Game'; import { useRouter } from 'vue-router'; import type { GameMessage } from '@/models/dto/messages/GameMessage'; @@ -16,9 +20,9 @@ import type { PlayerChoice } from '@/models/game/PlayerChoice'; export const useGameStore = defineStore('game', () => { const board = ref(null); const players = ref>([]); - const playerCount = computed( () => { + const playerCount = computed(() => { return players.value.length; - }) + }); const hostUsername = ref(''); const isHost = ref(false); const self = ref(null); @@ -54,7 +58,11 @@ export const useGameStore = defineStore('game', () => { board.value = b; } - function setGameProperties(game: Game, isHostAttr: boolean, playerSelf: Player | null){ + function setGameProperties( + game: Game, + isHostAttr: boolean, + playerSelf: Player | null, + ) { gameUuid = game.uuid; gameInviteCode.value = game.inviteCode; players.value = game.players; @@ -75,7 +83,7 @@ export const useGameStore = defineStore('game', () => { setGameProperties(game, true, null); return connect(); }) - .then( () => { + .then(() => { resolve(retGame); }) .catch((error: AxiosError) => { @@ -85,9 +93,9 @@ export const useGameStore = defineStore('game', () => { }); } - function checkForGame(): Game | null{ - const gameJson = sessionStorage.getItem(SESSION_GAME_KEY) - if( gameJson === null ){ + function checkForGame(): Game | null { + const gameJson = sessionStorage.getItem(SESSION_GAME_KEY); + if (gameJson === null) { return null; } else { return JSON.parse(gameJson) as Game; @@ -100,12 +108,12 @@ export const useGameStore = defineStore('game', () => { gameService .getGameByInviteCodeOrUuid(inviteCode) .then((game) => { - retGame = game - sessionStorage.setItem(SESSION_GAME_KEY, JSON.stringify(game)) + retGame = game; + sessionStorage.setItem(SESSION_GAME_KEY, JSON.stringify(game)); setGameProperties(game, false, null); return connect(); }) - .then( () => { + .then(() => { resolve(retGame); }) .catch((error: AxiosError) => { @@ -115,7 +123,7 @@ export const useGameStore = defineStore('game', () => { }); } function joinGame(playerName: string): Promise { - if( gameStatus.value === GAME_STATUS_CONST.nA ){ + if (gameStatus.value === GAME_STATUS_CONST.nA) { return Promise.reject(); } return new Promise((resolve, _reject) => { @@ -134,9 +142,9 @@ export const useGameStore = defineStore('game', () => { const stompClient = new StompClient({ brokerURL: 'ws://localhost:8008/ws', }); - const isConnected = computed( () => { + const isConnected = computed(() => { return stompClient.connected; - }) + }); stompClient.onStompError = (frame) => { console.error('Broker reported error: ' + frame.headers['message']); @@ -144,7 +152,7 @@ export const useGameStore = defineStore('game', () => { }; function connect(): Promise { - return new Promise(( resolve, reject ) => { + return new Promise((resolve, reject) => { stompClient.onConnect = (_frame) => { connected.value = true; subscribeToCommonRoutes(); @@ -152,10 +160,10 @@ export const useGameStore = defineStore('game', () => { }; stompClient.onWebSocketError = (error) => { console.error('Error with websocket', error); - reject() + reject(); }; stompClient.activate(); - }) + }); } function subscribeToCommonRoutes() { @@ -170,112 +178,149 @@ export const useGameStore = defineStore('game', () => { players.value.push(JSON.parse(message.body) as Player); }); stompClient.subscribe(`/topic/game/${gameUuid}/started`, (message) => { - const gameMessage = JSON.parse(message.body) as GameMessage - if( gameMessage.uuid !== gameUuid ){ - throw new Error("Got event for different Game!"); + const gameMessage = JSON.parse(message.body) as GameMessage; + if (gameMessage.uuid !== gameUuid) { + throw new Error('Got event for different Game!'); } - router.push({name: 'game', params: { uuid: gameUuid }}) + router.push({ name: 'game', params: { uuid: gameUuid } }); }); - stompClient.subscribe(`/topic/game/${gameUuid}/boardentry/selected`, (message) => { - const messageBody = JSON.parse(message.body); - if( !isHost.value ){ - showingAnswer.value = false; - showingQuestion.value = false; - acceptAnswers.value = true; - } else { - currentQuestion.value = messageBody.question; - } - }); - stompClient.subscribe(`/topic/game/${gameUuid}/board/selected`, (message) => { - if( !isHost.value ){ - showingAnswer.value = false; - showingQuestion.value = false; - acceptAnswers.value = false; - } - for( const i in players.value ){ - players.value[i].answerText = ''; - } - categoryIndex.value = null; - boardEntryIndex.value = null; - questionIndex.value = null; - }); - stompClient.subscribe(`/topic/game/${gameUuid}/points/adjusted`, (message) => { - const messageBody = JSON.parse(message.body); - const playerIndex = players.value.findIndex( playerEntry => playerEntry.uuid === messageBody.playerUuid ); - players.value[playerIndex].points = messageBody.adjustedPoints; - }); - stompClient.subscribe(`/topic/game/${gameUuid}/player/answer/revealed`, (message) => { - currentAnswer.value = JSON.parse(message.body) as Answer; - }); - stompClient.subscribe(`/topic/game/${gameUuid}/player/answer/update`, (message) => { - const messageBody = JSON.parse(message.body); - const playerIndex = players.value.findIndex( playerEntry => playerEntry.uuid === messageBody.playerUuid ); - players.value[playerIndex].answerText = messageBody.answerText; - }); - stompClient.subscribe(`/topic/game/${gameUuid}/player/buzzered`, (message) => { - const messageBody = JSON.parse(message.body); - for( const i in players.value ){ - if( players.value[i].uuid === messageBody.playerUuid ){ - players.value[i].isAnswering = true; - //TODO Play Buzzer sound! + stompClient.subscribe( + `/topic/game/${gameUuid}/boardentry/selected`, + (message) => { + const messageBody = JSON.parse(message.body); + if (!isHost.value) { + showingAnswer.value = false; + showingQuestion.value = false; + acceptAnswers.value = true; } else { - players.value[i].isAnswering = false; + currentQuestion.value = messageBody.question; } - } - acceptAnswers.value = false; - //TODO if( audioInstance ) If audio was playing - stop! - }); + }, + ); + stompClient.subscribe( + `/topic/game/${gameUuid}/board/selected`, + (message) => { + if (!isHost.value) { + showingAnswer.value = false; + showingQuestion.value = false; + acceptAnswers.value = false; + } + for (const i in players.value) { + players.value[i].answerText = ''; + } + categoryIndex.value = null; + boardEntryIndex.value = null; + questionIndex.value = null; + }, + ); + stompClient.subscribe( + `/topic/game/${gameUuid}/points/adjusted`, + (message) => { + const messageBody = JSON.parse(message.body); + const playerIndex = players.value.findIndex( + (playerEntry) => playerEntry.uuid === messageBody.playerUuid, + ); + players.value[playerIndex].points = messageBody.adjustedPoints; + }, + ); + stompClient.subscribe( + `/topic/game/${gameUuid}/player/answer/revealed`, + (message) => { + currentAnswer.value = JSON.parse(message.body) as Answer; + }, + ); + stompClient.subscribe( + `/topic/game/${gameUuid}/player/answer/update`, + (message) => { + const messageBody = JSON.parse(message.body); + const playerIndex = players.value.findIndex( + (playerEntry) => playerEntry.uuid === messageBody.playerUuid, + ); + players.value[playerIndex].answerText = messageBody.answerText; + }, + ); + stompClient.subscribe( + `/topic/game/${gameUuid}/player/buzzered`, + (message) => { + const messageBody = JSON.parse(message.body); + for (const i in players.value) { + if (players.value[i].uuid === messageBody.playerUuid) { + players.value[i].isAnswering = true; + //TODO Play Buzzer sound! + } else { + players.value[i].isAnswering = false; + } + } + acceptAnswers.value = false; + //TODO if( audioInstance ) If audio was playing - stop! + }, + ); stompClient.subscribe(`/topic/game/${gameUuid}/player/chose`, (message) => { const messageBody = JSON.parse(message.body) as null | PlayerChoice; currentPlayerChoice.value = messageBody; }); - stompClient.subscribe(`/topic/game/${gameUuid}/player/canchoose`, (message) => { - }); + stompClient.subscribe( + `/topic/game/${gameUuid}/player/canchoose`, + (message) => {}, + ); stompClient.subscribe(`/topic/game/${gameUuid}/player/left`, (message) => { const messageBody = JSON.parse(message.body); - const playerIndex = players.value.findIndex( playerEntry => playerEntry.uuid === messageBody.playerUuid ); + const playerIndex = players.value.findIndex( + (playerEntry) => playerEntry.uuid === messageBody.playerUuid, + ); players.value.splice(playerIndex, 1); }); - stompClient.subscribe(`/topic/game/${gameUuid}/question/audio/playing`, (message) => { - - }); - stompClient.subscribe(`/topic/game/${gameUuid}/question/audio/stopped`, (message) => { - - }); - stompClient.subscribe(`/topic/game/${gameUuid}/question/revealed`, (message) => { - currentQuestion.value = JSON.parse(message.body) as Question; - }); - stompClient.subscribe(`/topic/game/${gameUuid}/question/locked`, (message) => { - - }); - stompClient.subscribe(`/topic/game/${gameUuid}/question/status/update`, (message) => { - - }); - stompClient.subscribe(`/topic/game/${gameUuid}/question/hidden`, (message) => { - - }); - stompClient.subscribe(`/topic/game/${gameUuid}/questionlayer/selected`, (message) => { - - }); - stompClient.subscribe(`/topic/game/${gameUuid}/answer/revealed`, (message) => { - - }); - stompClient.subscribe(`/topic/game/${gameUuid}/answer/hidden`, (message) => { - - }); + stompClient.subscribe( + `/topic/game/${gameUuid}/question/audio/playing`, + (message) => {}, + ); + stompClient.subscribe( + `/topic/game/${gameUuid}/question/audio/stopped`, + (message) => {}, + ); + stompClient.subscribe( + `/topic/game/${gameUuid}/question/revealed`, + (message) => { + currentQuestion.value = JSON.parse(message.body) as Question; + }, + ); + stompClient.subscribe( + `/topic/game/${gameUuid}/question/locked`, + (message) => {}, + ); + stompClient.subscribe( + `/topic/game/${gameUuid}/question/status/update`, + (message) => {}, + ); + stompClient.subscribe( + `/topic/game/${gameUuid}/question/hidden`, + (message) => {}, + ); + stompClient.subscribe( + `/topic/game/${gameUuid}/questionlayer/selected`, + (message) => {}, + ); + stompClient.subscribe( + `/topic/game/${gameUuid}/answer/revealed`, + (message) => {}, + ); + stompClient.subscribe( + `/topic/game/${gameUuid}/answer/hidden`, + (message) => {}, + ); } function disconnect() { stompClient.deactivate(); connected.value = false; } - + function startGame() { - if( !isHost.value ){ + if (!isHost.value) { return; } const message = { - content: 'start' + content: 'start', }; stompClient.publish({ destination: `/app/host/game/${gameUuid}/start`,