5 Commits

32 changed files with 379 additions and 48 deletions

View File

@@ -10,16 +10,9 @@ REST API to support a community scavenger hunt app.
## TODO:
### User Endpoints
* list teams for hunt GET `/hunt/{id}/team`
* create new hunt team POST `/hunt/{id}/team`
* join hunt team POST `/hunt/{id}/team/{id}`
* list items for hunt GET `/hunt/{id}/item`
* get hunt item info GET `/hunt/{id}/item/{id}`
* get hunt team item info GET `/hunt/{id}/team/{id}/item/{id}`
* get photos for hunt item GET `/hunt/{id}/team/{id}/item/{id}/photo`
* upload photo for hunt item POST `/hunt/{id}/team/{id}/item/{id}/photo`
* delete photo for hunt item DELETE `/hunt/{id}/team/{id}/item/{id}/photo`
* list hunt teams with scores for hunt `GET /lead/hunt/{id}/team`
* list hunters with scores for hunt GET `/lead/hunt/{id}/hunter`
* upload photo for hunt item POST `/hunt/{huntId}/team/{teamId}/item/{itemId}/photo` - body: image binary
* delete photo for hunt item DELETE `/hunt/{huntId}/team/{teamId}/item/{itemId}/photo/{photoId}`
* list hunt teams with scores for hunt `GET /lead/hunt/{huntId}/team`
* list hunters with scores for hunt GET `/lead/hunt/{huntId}/hunter`
### Admin Endpoints
* approve photo for hunt item POST `/admin/hunt/{id}/team/{id}`
* approve photo for hunt item POST `/admin/hunt/{huntId}/team/{teamId}/item/{itemId}/photo/{photoId}` - body: approval status

View File

@@ -1,9 +1,9 @@
plugins {
kotlin("jvm") version "2.2.21"
kotlin("plugin.spring") version "2.2.21"
id("org.springframework.boot") version "4.0.0"
kotlin("jvm") version "2.3.21"
kotlin("plugin.spring") version "2.3.21"
id("org.springframework.boot") version "4.0.6"
id("io.spring.dependency-management") version "1.1.7"
kotlin("plugin.jpa") version "2.2.21"
kotlin("plugin.jpa") version "2.3.21"
}
group = "net.halfbinary"
@@ -27,23 +27,25 @@ repositories {
}
dependencies {
val mysqlConnectorJ = "9.5.0"
val mariaDriver = "3.5.8"
val commonsValidator = "1.10.1"
val jakartaValidation = "3.1.1"
val jsonWebToken = "0.13.0"
val springdocUi = "3.0.3"
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("jakarta.validation:jakarta.validation-api:${jakartaValidation}")
implementation("com.mysql:mysql-connector-j:${mysqlConnectorJ}")
implementation("jakarta.validation:jakarta.validation-api:$jakartaValidation")
implementation("org.mariadb.jdbc:mariadb-java-client:${mariaDriver}")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("commons-validator:commons-validator:${commonsValidator}")
implementation("io.jsonwebtoken:jjwt-api:${jsonWebToken}")
implementation("io.jsonwebtoken:jjwt-impl:${jsonWebToken}")
implementation("io.jsonwebtoken:jjwt-jackson:${jsonWebToken}")
implementation("commons-validator:commons-validator:$commonsValidator")
implementation("io.jsonwebtoken:jjwt-api:$jsonWebToken")
implementation("io.jsonwebtoken:jjwt-impl:$jsonWebToken")
implementation("io.jsonwebtoken:jjwt-jackson:$jsonWebToken")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:$springdocUi")
developmentOnly("org.springframework.boot:spring-boot-devtools")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
testImplementation("org.springframework.boot:spring-boot-starter-actuator-test")

Binary file not shown.

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

2
gradlew vendored
View File

@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/b631911858264c0b6e4d6603d677ff5218766cee/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.

View File

@@ -31,7 +31,9 @@ class AuthTokenFilter(private val jwtUtils: JwtUtil, private val hunterDetailsSe
userDetails.authorities
)
authentication.details = WebAuthenticationDetailsSource().buildDetails(request)
SecurityContextHolder.getContext().authentication = authentication
val context = SecurityContextHolder.createEmptyContext()
context.authentication = authentication
SecurityContextHolder.setContext(context)
}
} catch (e: Exception) {
println("Cannot set user authentication: $e")

View File

@@ -7,11 +7,12 @@ import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
import java.util.Date
import javax.crypto.SecretKey
import javax.crypto.spec.SecretKeySpec
@Component
class JwtUtil {
@Value($$"${jwt.secret}")
private val jwtSecret: String? = null
private val jwtSecret: String = ""
@Value($$"${jwt.expiration}")
private val jwtExpirationMs = 0
@@ -22,7 +23,7 @@ class JwtUtil {
// preventing the repeated creation of the key and enhancing performance
@PostConstruct
fun init() {
this.key = Jwts.SIG.HS256.key().build()
this.key = SecretKeySpec(jwtSecret.toByteArray(Charsets.UTF_8), "HmacSHA256")
}
// Generate JWT token

View File

@@ -1,5 +1,6 @@
package net.halfbinary.scavengerhuntapi.config
import org.springframework.boot.web.servlet.FilterRegistrationBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.authentication.AuthenticationManager
@@ -27,6 +28,13 @@ class SecurityConfig(private val authEntrypointJwt: AuthEntrypointJwt,
return authTokenFilter
}
@Bean
fun authTokenFilterRegistration(): FilterRegistrationBean<AuthTokenFilter> {
val registration = FilterRegistrationBean(authTokenFilter)
registration.isEnabled = false
return registration
}
@Bean
@Throws(Exception::class)
fun authenticationManager(
@@ -59,7 +67,7 @@ class SecurityConfig(private val authEntrypointJwt: AuthEntrypointJwt,
}
.authorizeHttpRequests { authorizeRequests ->
authorizeRequests
.requestMatchers("/auth/**", "/signup")
.requestMatchers("/auth/**", "/signup", "/docs/**")
.permitAll()
.anyRequest().authenticated()
}

View File

@@ -4,10 +4,12 @@ import jakarta.servlet.http.HttpServletResponse
import jakarta.validation.Valid
import net.halfbinary.scavengerhuntapi.config.JwtUtil
import net.halfbinary.scavengerhuntapi.model.converter.toDomain
import net.halfbinary.scavengerhuntapi.model.converter.toRefreshResponse
import net.halfbinary.scavengerhuntapi.model.request.LoginRequest
import net.halfbinary.scavengerhuntapi.model.request.LogoutRequest
import net.halfbinary.scavengerhuntapi.model.request.RefreshRequest
import net.halfbinary.scavengerhuntapi.model.response.LoginResponse
import net.halfbinary.scavengerhuntapi.model.response.RefreshResponse
import net.halfbinary.scavengerhuntapi.service.LoginService
import net.halfbinary.scavengerhuntapi.service.RefreshTokenService
import org.springframework.http.ResponseEntity
@@ -26,7 +28,6 @@ class AuthController(private val loginService: LoginService, private val jwtUtil
@PostMapping("/login")
fun login(@Valid @RequestBody body: LoginRequest, response: HttpServletResponse): ResponseEntity<LoginResponse> {
val result = loginService.login(body.toDomain())
// TODO: Figure out how to use the authorities
val hunterAuthorities =
if (result.isAdmin) {
SimpleGrantedAuthority("ROLE_ADMIN")
@@ -41,8 +42,8 @@ class AuthController(private val loginService: LoginService, private val jwtUtil
}
@PostMapping("/refresh")
fun refresh(@RequestBody body: RefreshRequest): String {
return refreshTokenService.getAccessToken(body.refreshToken)
fun refresh(@RequestBody body: RefreshRequest): ResponseEntity<RefreshResponse> {
return ResponseEntity.ok(refreshTokenService.getAccessToken(body.refreshToken).toRefreshResponse())
}
@PostMapping("/logout")

View File

@@ -1,5 +1,7 @@
package net.halfbinary.scavengerhuntapi.controller
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.validation.Valid
import net.halfbinary.scavengerhuntapi.model.HuntId
import net.halfbinary.scavengerhuntapi.model.HunterId
@@ -9,32 +11,61 @@ import net.halfbinary.scavengerhuntapi.model.request.HuntCreateRequest
import net.halfbinary.scavengerhuntapi.model.request.HuntStatus
import net.halfbinary.scavengerhuntapi.model.response.HuntResponse
import net.halfbinary.scavengerhuntapi.service.HuntService
import net.halfbinary.scavengerhuntapi.service.HunterService
import org.springframework.http.ResponseEntity
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.security.core.Authentication
import org.springframework.web.bind.annotation.*
import java.time.LocalDateTime
@RestController
@RequestMapping("hunt")
class HuntController(private val huntService: HuntService) {
class HuntController(private val huntService: HuntService, private val hunterService: HunterService) {
@GetMapping("/{id}")
@Operation(summary = "Gets the specified hunt information")
fun getHunt(@PathVariable("id") huntId: HuntId): ResponseEntity<HuntResponse> {
return ResponseEntity.ok(huntService.getHunt(huntId).toResponse())
}
@PreAuthorize("hasRole('ADMIN')")
@Tag(name = "Admin")
@GetMapping()
@Operation(summary = "Gets all Hunts")
fun getAllHunts(@RequestParam status: HuntStatus?): ResponseEntity<List<HuntResponse>> {
return ResponseEntity.ok(huntService.getAllHunts(status).map { it.toResponse() })
}
@GetMapping("/ongoing")
@Operation(summary = "Gets list of all currently running Hunts (filtered by the calling hunter)")
fun getOngoingHunts(authentication: Authentication, @RequestParam status: HuntStatus?): ResponseEntity<List<HuntResponse>> {
val email = authentication.name
val isAdmin = hunterService.getHunterByEmail(email).isAdmin
return if(isAdmin) {
ResponseEntity.ok(huntService.getAllHunts(HuntStatus.ONGOING).map { it.toResponse() })
} else {
ResponseEntity.ok(huntService.getHuntsByEmail(email, status).map { it.toResponse() })
}
}
@GetMapping("/unstarted")
@Operation(summary = "Gets list of all upcoming Hunts")
fun getUnstartedHunts(): ResponseEntity<List<HuntResponse>> {
return ResponseEntity.ok(huntService.getAllHunts(HuntStatus.UNSTARTED).map { it.toResponse() })
}
@PreAuthorize("hasRole('ADMIN')")
@Tag(name = "Admin")
@PostMapping()
@Operation(summary = "Creates a new Hunt")
fun createHunt(@Valid @RequestBody huntRequest: HuntCreateRequest): ResponseEntity<HuntResponse> {
return ResponseEntity.ok(huntService.createHunt(huntRequest.toDomain()).toResponse())
}
@PreAuthorize("hasRole('ADMIN')")
@Tag(name = "Admin")
@GetMapping("/hunter/{hunterId}")
fun getHuntsByHunter(@PathVariable("hunterId") hunterId: HunterId): ResponseEntity<List<HuntResponse>> {
@Operation(summary = "Lists all Hunts for specified Hunter")
fun getHuntsByHunter(@PathVariable hunterId: HunterId): ResponseEntity<List<HuntResponse>> {
return ResponseEntity.ok(huntService.getHuntsByHunter(hunterId).map { it.toResponse() })
}

View File

@@ -0,0 +1,41 @@
package net.halfbinary.scavengerhuntapi.controller
import io.swagger.v3.oas.annotations.Operation
import jakarta.validation.Valid
import net.halfbinary.scavengerhuntapi.model.HuntId
import net.halfbinary.scavengerhuntapi.model.HunterId
import net.halfbinary.scavengerhuntapi.model.ItemId
import net.halfbinary.scavengerhuntapi.model.TeamHuntId
import net.halfbinary.scavengerhuntapi.model.converter.toDomain
import net.halfbinary.scavengerhuntapi.model.converter.toResponse
import net.halfbinary.scavengerhuntapi.model.request.HuntCreateRequest
import net.halfbinary.scavengerhuntapi.model.request.HuntStatus
import net.halfbinary.scavengerhuntapi.model.request.ItemRequest
import net.halfbinary.scavengerhuntapi.model.response.HuntResponse
import net.halfbinary.scavengerhuntapi.model.response.ItemResponse
import net.halfbinary.scavengerhuntapi.service.HuntService
import org.springframework.http.ResponseEntity
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("hunt/{huntId}/item")
class ItemController(private val huntService: HuntService) {
@GetMapping
fun getItemsForHunt(@PathVariable huntId: HuntId): ResponseEntity<List<ItemResponse>> {
TODO()
}
@GetMapping("/{itemId}")
fun getItem(@PathVariable huntId: HuntId, @PathVariable itemId: ItemId): ResponseEntity<ItemResponse> {
TODO()
}
@PostMapping
@Operation(summary = "Adds new Item to specified Hunt")
fun addItemToHunt(@PathVariable huntId: HuntId, @Valid @RequestBody body: ItemRequest) {
TODO()
}
}

View File

@@ -1,10 +1,18 @@
package net.halfbinary.scavengerhuntapi.controller
import io.swagger.v3.oas.annotations.Operation
import jakarta.validation.Valid
import net.halfbinary.scavengerhuntapi.model.HuntId
import net.halfbinary.scavengerhuntapi.model.ItemId
import net.halfbinary.scavengerhuntapi.model.TeamId
import net.halfbinary.scavengerhuntapi.model.converter.toResponse
import net.halfbinary.scavengerhuntapi.model.request.TeamRequest
import net.halfbinary.scavengerhuntapi.model.response.PhotoResponse
import net.halfbinary.scavengerhuntapi.model.response.TeamItemResponse
import net.halfbinary.scavengerhuntapi.model.response.TeamResponse
import net.halfbinary.scavengerhuntapi.service.TeamService
import org.springframework.http.ResponseEntity
import org.springframework.security.core.Authentication
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
@@ -13,15 +21,44 @@ import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("hunt/{id}/team")
class TeamController {
@RequestMapping("hunt/{huntId}/team")
class TeamController(private val teamService: TeamService) {
@GetMapping
fun listHuntTeams(@PathVariable id: HuntId): ResponseEntity<List<TeamResponse>> {
TODO()
@Operation(summary = "List all teams for the specified hunt")
fun listHuntTeams(@PathVariable huntId: HuntId): ResponseEntity<List<TeamResponse>> {
return ResponseEntity.ok(teamService.getListOfTeamsForHunt(huntId).map { it.toResponse()})
}
@PostMapping
fun createHuntTeam(@PathVariable id: HuntId, @Valid @RequestBody team: TeamRequest) {
@Operation(summary = "Create a new team for the specified hunt")
fun createHuntTeam(@PathVariable huntId: HuntId, @Valid @RequestBody team: TeamRequest) {
val teamResponse = teamService.createTeam(team.name)
teamService.addTeamToHunt(huntId, teamResponse.id)
}
@GetMapping("/{teamId}")
fun getTeam(@PathVariable huntId: HuntId, @PathVariable teamId: TeamId): ResponseEntity<TeamResponse> {
return ResponseEntity.ok(teamService.getTeamFromHunt(huntId, teamId).toResponse())
}
@PostMapping("/{teamId}")
fun joinTeamForHunt(@PathVariable huntId: HuntId, @PathVariable teamId: TeamId, authentication: Authentication) {
teamService.joinTeam(teamId, authentication.name)
}
@GetMapping("/{teamId}/item/{itemId}")
fun getItemsForTeam(@PathVariable huntId: HuntId,
@PathVariable teamId: TeamId,
@PathVariable itemId: ItemId): ResponseEntity<TeamItemResponse> {
TODO()
}
@GetMapping("/{teamId}/item/{itemId}/photo")
fun getPhotosForTeam(@PathVariable huntId: HuntId,
@PathVariable teamId: TeamId,
@PathVariable itemId: ItemId): ResponseEntity<PhotoResponse> {
TODO()
}
}

View File

@@ -1,6 +1,7 @@
package net.halfbinary.scavengerhuntapi.model
enum class FoundStatus {
NOT_FOUND,
SUBMITTED,
APPROVED,
REJECTED,

View File

@@ -7,4 +7,6 @@ typealias HuntId = UUID
typealias HunterId = UUID
typealias ItemId = UUID
typealias TeamId = UUID
typealias RefreshId = UUID
typealias RefreshId = UUID
typealias TeamHuntId = UUID
typealias PhotoId = UUID

View File

@@ -0,0 +1,7 @@
package net.halfbinary.scavengerhuntapi.model.converter
import net.halfbinary.scavengerhuntapi.model.response.RefreshResponse
fun String.toRefreshResponse(): RefreshResponse {
return RefreshResponse(this)
}

View File

@@ -0,0 +1,16 @@
package net.halfbinary.scavengerhuntapi.model.converter
import net.halfbinary.scavengerhuntapi.model.domain.Team
import net.halfbinary.scavengerhuntapi.model.domain.TeamHunt
import net.halfbinary.scavengerhuntapi.model.record.TeamHuntRecord
import net.halfbinary.scavengerhuntapi.model.record.TeamRecord
import net.halfbinary.scavengerhuntapi.model.request.TeamRequest
import net.halfbinary.scavengerhuntapi.model.response.TeamResponse
fun TeamHunt.toRecord(): TeamHuntRecord {
return TeamHuntRecord(id, teamId, huntId)
}
fun TeamHuntRecord.toDomain(): TeamHunt {
return TeamHunt(id, teamId, huntId)
}

View File

@@ -0,0 +1,12 @@
package net.halfbinary.scavengerhuntapi.model.domain
import net.halfbinary.scavengerhuntapi.model.HuntId
import net.halfbinary.scavengerhuntapi.model.TeamHuntId
import net.halfbinary.scavengerhuntapi.model.TeamId
import java.util.UUID
data class TeamHunt(
val id: TeamHuntId = TeamHuntId.randomUUID(),
val teamId: TeamId,
val huntId: HuntId
)

View File

@@ -4,6 +4,7 @@ import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.Table
import net.halfbinary.scavengerhuntapi.model.HuntId
import net.halfbinary.scavengerhuntapi.model.TeamHuntId
import net.halfbinary.scavengerhuntapi.model.TeamId
import java.util.*
@@ -11,7 +12,7 @@ import java.util.*
@Table(name = "team_hunt")
data class TeamHuntRecord(
@Id
val id: UUID,
val id: TeamHuntId,
val teamId: TeamId,
val huntId: HuntId
)

View File

@@ -0,0 +1,6 @@
package net.halfbinary.scavengerhuntapi.model.request
data class ItemRequest(
val name: String,
val points: Int
)

View File

@@ -0,0 +1,9 @@
package net.halfbinary.scavengerhuntapi.model.request
import jakarta.validation.constraints.NotBlank
import net.halfbinary.scavengerhuntapi.model.TeamId
data class JoinTeamRequest(
@field:NotBlank
val teamId: TeamId
)

View File

@@ -0,0 +1,9 @@
package net.halfbinary.scavengerhuntapi.model.response
import net.halfbinary.scavengerhuntapi.model.ItemId
data class ItemResponse(
val id: ItemId,
val name: String,
val points: Int
)

View File

@@ -0,0 +1,10 @@
package net.halfbinary.scavengerhuntapi.model.response
import net.halfbinary.scavengerhuntapi.model.PhotoId
import java.time.LocalDateTime
data class PhotoResponse(
val id: PhotoId,
val hunterName: String,
val photoUploadDateTime: LocalDateTime
)

View File

@@ -0,0 +1,5 @@
package net.halfbinary.scavengerhuntapi.model.response
data class RefreshResponse(
val accessToken: String
)

View File

@@ -0,0 +1,11 @@
package net.halfbinary.scavengerhuntapi.model.response
import net.halfbinary.scavengerhuntapi.model.FoundStatus
import net.halfbinary.scavengerhuntapi.model.ItemId
data class TeamItemResponse(
val id: ItemId,
val itemName: String,
val hunterName: String,
val itemFoundStatus: FoundStatus
)

View File

@@ -2,6 +2,7 @@ package net.halfbinary.scavengerhuntapi.repository
import net.halfbinary.scavengerhuntapi.model.HuntId
import net.halfbinary.scavengerhuntapi.model.HunterId
import net.halfbinary.scavengerhuntapi.model.TeamId
import net.halfbinary.scavengerhuntapi.model.record.HuntRecord
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
@@ -23,6 +24,17 @@ interface HuntRepository : JpaRepository<HuntRecord, HuntId> {
""", nativeQuery = true)
fun findAllOngoingByHunter(hunterId: HunterId): List<HuntRecord>
@Query("""
SELECT h.*
FROM hunter u
INNER JOIN hunter_team ht ON u.id = ht.hunter_id
INNER JOIN team t ON ht.team_id = t.id
INNER JOIN team_hunt th ON t.id = th.team_id
INNER JOIN hunt h ON th.hunt_id = h.id
WHERE u.email = :email
""", nativeQuery = true)
fun findHuntsByEmail(email: String): List<HuntRecord>
@Query("""
SELECT h.*
FROM hunt h

View File

@@ -0,0 +1,9 @@
package net.halfbinary.scavengerhuntapi.repository
import net.halfbinary.scavengerhuntapi.model.record.HunterTeamRecord
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import java.util.UUID
@Repository
interface HunterTeamRepository : JpaRepository<HunterTeamRecord, UUID>

View File

@@ -0,0 +1,28 @@
package net.halfbinary.scavengerhuntapi.repository
import net.halfbinary.scavengerhuntapi.model.HuntId
import net.halfbinary.scavengerhuntapi.model.TeamId
import net.halfbinary.scavengerhuntapi.model.record.HuntRecord
import net.halfbinary.scavengerhuntapi.model.record.TeamHuntRecord
import net.halfbinary.scavengerhuntapi.model.record.TeamRecord
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.stereotype.Repository
import java.util.*
@Repository
interface TeamHuntRepository : JpaRepository<TeamHuntRecord, UUID> {
@Query("""
SELECT h.* FROM hunt h
INNER JOIN team_hunt th ON h.id = th.hunt_id
WHERE th.team_id = :teamId
""", nativeQuery = true)
fun findHuntsByTeamId(teamId: TeamId): List<HuntRecord>
@Query("""
SELECT t.* FROM team t
INNER JOIN team_hunt th ON t.id = th.team_id
WHERE th.hunt_id = :huntId
""", nativeQuery = true)
fun findTeamsByHuntId(huntId: HuntId): List<TeamRecord>
}

View File

@@ -1,5 +1,6 @@
package net.halfbinary.scavengerhuntapi.repository
import net.halfbinary.scavengerhuntapi.model.HuntId
import net.halfbinary.scavengerhuntapi.model.TeamId
import net.halfbinary.scavengerhuntapi.model.record.TeamRecord
import org.springframework.data.jpa.repository.JpaRepository

View File

@@ -10,6 +10,7 @@ import net.halfbinary.scavengerhuntapi.model.request.HuntStatus
import net.halfbinary.scavengerhuntapi.repository.HuntRepository
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import java.time.LocalDateTime
@Service
class HuntService(private val huntRepository: HuntRepository) {
@@ -30,6 +31,27 @@ class HuntService(private val huntRepository: HuntRepository) {
return huntRepository.findAllOngoingByHunter(hunterId).map { it.toDomain() }
}
fun getHuntsByEmail(email: String, status: HuntStatus?): List<Hunt> {
val allHunts = huntRepository.findHuntsByEmail(email)
val filteredHunts = when (status) {
HuntStatus.ONGOING -> {
allHunts
.filter { !it.isTerminated && it.startDateTime < LocalDateTime.now() && it.endDateTime > LocalDateTime.now() }
.toList()
}
HuntStatus.CLOSED -> {
allHunts
.filter { it.isTerminated || it.endDateTime < LocalDateTime.now() }
}
HuntStatus.UNSTARTED -> {
allHunts
.filter { !it.isTerminated && it.startDateTime > LocalDateTime.now() }
}
else -> { allHunts }
}
return filteredHunts.map { it.toDomain() }
}
fun createHunt(hunt: Hunt): Hunt {
return huntRepository.save(hunt.toRecord()).toDomain()
}

View File

@@ -0,0 +1,15 @@
package net.halfbinary.scavengerhuntapi.service
import net.halfbinary.scavengerhuntapi.error.exception.NotFoundException
import net.halfbinary.scavengerhuntapi.model.converter.toDomain
import net.halfbinary.scavengerhuntapi.model.domain.Hunter
import net.halfbinary.scavengerhuntapi.repository.HunterRepository
import org.springframework.stereotype.Service
@Service
class HunterService(private val hunterRepository: HunterRepository) {
fun getHunterByEmail(email: String): Hunter {
return hunterRepository.findByEmail(email)?.toDomain()
?: throw NotFoundException("No hunter with email $email found")
}
}

View File

@@ -1,21 +1,55 @@
package net.halfbinary.scavengerhuntapi.service
import net.halfbinary.scavengerhuntapi.error.exception.NotFoundException
import net.halfbinary.scavengerhuntapi.model.HuntId
import net.halfbinary.scavengerhuntapi.model.TeamId
import net.halfbinary.scavengerhuntapi.model.converter.toDomain
import net.halfbinary.scavengerhuntapi.model.converter.toRecord
import net.halfbinary.scavengerhuntapi.model.domain.Team
import net.halfbinary.scavengerhuntapi.model.domain.TeamHunt
import net.halfbinary.scavengerhuntapi.model.record.HunterTeamRecord
import net.halfbinary.scavengerhuntapi.model.record.TeamHuntRecord
import net.halfbinary.scavengerhuntapi.model.record.TeamRecord
import net.halfbinary.scavengerhuntapi.model.request.TeamRequest
import net.halfbinary.scavengerhuntapi.model.response.TeamResponse
import net.halfbinary.scavengerhuntapi.repository.HunterRepository
import net.halfbinary.scavengerhuntapi.repository.HunterTeamRepository
import net.halfbinary.scavengerhuntapi.repository.TeamHuntRepository
import net.halfbinary.scavengerhuntapi.repository.TeamRepository
import org.springframework.stereotype.Service
import java.util.UUID
@Service
class TeamService {
class TeamService(
private val teamRepository: TeamRepository,
private val teamHuntRepository: TeamHuntRepository,
private val hunterRepository: HunterRepository,
private val hunterTeamRepository: HunterTeamRepository,
) {
fun getListOfTeamsForHunt(huntId: HuntId): List<Team> {
TODO()
return getTeamsForHunt(huntId)
}
fun createTeam(name: String): Team {
TODO()
return teamRepository.save(TeamRequest(name).toDomain().toRecord()).toDomain()
}
fun addTeamToHunt(huntId: HuntId, teamId: TeamId) {
TODO()
teamHuntRepository.save(TeamHunt(teamId = teamId, huntId = huntId).toRecord()).toDomain()
}
fun getTeamFromHunt(huntId: HuntId, teamId: TeamId): Team {
return getTeamsForHunt(huntId)
.filter { it.id == teamId }
.elementAt(0)
}
fun joinTeam(teamId: TeamId, email: String) {
val hunter = hunterRepository.findByEmail(email) ?: throw NotFoundException("No hunter with email $email found")
hunterTeamRepository.save(HunterTeamRecord(UUID.randomUUID(), hunter.id, teamId))
}
private fun getTeamsForHunt(huntId: HuntId): List<Team> {
return teamHuntRepository.findTeamsByHuntId(huntId).map { it.toDomain() }
}
}

View File

@@ -4,10 +4,15 @@ spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.type.preferred_uuid_jdbc_type=CHAR
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.driverClassName=org.mariadb.jdbc.Driver
spring.datasource.url=${DB_URL}
spring.datasource.username=${DB_USER}
spring.datasource.password=${DB_PASSWORD}
jwt.secret=${JWT_SECRET}
jwt.expiration=30000
jwt.expiration=300000
springdoc.api-docs.enabled=true
springdoc.api-docs.path=/docs/api-docs
springdoc.swagger-ui.enabled=true
springdoc.swagger-ui.path=/docs/swagger-ui.html