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: ## TODO:
### User Endpoints ### User Endpoints
* list teams for hunt GET `/hunt/{id}/team` * upload photo for hunt item POST `/hunt/{huntId}/team/{teamId}/item/{itemId}/photo` - body: image binary
* create new hunt team POST `/hunt/{id}/team` * delete photo for hunt item DELETE `/hunt/{huntId}/team/{teamId}/item/{itemId}/photo/{photoId}`
* join hunt team POST `/hunt/{id}/team/{id}` * list hunt teams with scores for hunt `GET /lead/hunt/{huntId}/team`
* list items for hunt GET `/hunt/{id}/item` * list hunters with scores for hunt GET `/lead/hunt/{huntId}/hunter`
* 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`
### Admin Endpoints ### 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 { plugins {
kotlin("jvm") version "2.2.21" kotlin("jvm") version "2.3.21"
kotlin("plugin.spring") version "2.2.21" kotlin("plugin.spring") version "2.3.21"
id("org.springframework.boot") version "4.0.0" id("org.springframework.boot") version "4.0.6"
id("io.spring.dependency-management") version "1.1.7" 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" group = "net.halfbinary"
@@ -27,23 +27,25 @@ repositories {
} }
dependencies { dependencies {
val mysqlConnectorJ = "9.5.0" val mariaDriver = "3.5.8"
val commonsValidator = "1.10.1" val commonsValidator = "1.10.1"
val jakartaValidation = "3.1.1" val jakartaValidation = "3.1.1"
val jsonWebToken = "0.13.0" 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-actuator")
implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-validation") implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-security")
implementation("jakarta.validation:jakarta.validation-api:${jakartaValidation}") implementation("jakarta.validation:jakarta.validation-api:$jakartaValidation")
implementation("com.mysql:mysql-connector-j:${mysqlConnectorJ}") implementation("org.mariadb.jdbc:mariadb-java-client:${mariaDriver}")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("commons-validator:commons-validator:${commonsValidator}") implementation("commons-validator:commons-validator:$commonsValidator")
implementation("io.jsonwebtoken:jjwt-api:${jsonWebToken}") implementation("io.jsonwebtoken:jjwt-api:$jsonWebToken")
implementation("io.jsonwebtoken:jjwt-impl:${jsonWebToken}") implementation("io.jsonwebtoken:jjwt-impl:$jsonWebToken")
implementation("io.jsonwebtoken:jjwt-jackson:${jsonWebToken}") implementation("io.jsonwebtoken:jjwt-jackson:$jsonWebToken")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:$springdocUi")
developmentOnly("org.springframework.boot:spring-boot-devtools") developmentOnly("org.springframework.boot:spring-boot-devtools")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
testImplementation("org.springframework.boot:spring-boot-starter-actuator-test") testImplementation("org.springframework.boot:spring-boot-starter-actuator-test")

Binary file not shown.

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

2
gradlew vendored
View File

@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (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. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # 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 userDetails.authorities
) )
authentication.details = WebAuthenticationDetailsSource().buildDetails(request) authentication.details = WebAuthenticationDetailsSource().buildDetails(request)
SecurityContextHolder.getContext().authentication = authentication val context = SecurityContextHolder.createEmptyContext()
context.authentication = authentication
SecurityContextHolder.setContext(context)
} }
} catch (e: Exception) { } catch (e: Exception) {
println("Cannot set user authentication: $e") 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 org.springframework.stereotype.Component
import java.util.Date import java.util.Date
import javax.crypto.SecretKey import javax.crypto.SecretKey
import javax.crypto.spec.SecretKeySpec
@Component @Component
class JwtUtil { class JwtUtil {
@Value($$"${jwt.secret}") @Value($$"${jwt.secret}")
private val jwtSecret: String? = null private val jwtSecret: String = ""
@Value($$"${jwt.expiration}") @Value($$"${jwt.expiration}")
private val jwtExpirationMs = 0 private val jwtExpirationMs = 0
@@ -22,7 +23,7 @@ class JwtUtil {
// preventing the repeated creation of the key and enhancing performance // preventing the repeated creation of the key and enhancing performance
@PostConstruct @PostConstruct
fun init() { fun init() {
this.key = Jwts.SIG.HS256.key().build() this.key = SecretKeySpec(jwtSecret.toByteArray(Charsets.UTF_8), "HmacSHA256")
} }
// Generate JWT token // Generate JWT token

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
package net.halfbinary.scavengerhuntapi.controller 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 jakarta.validation.Valid
import net.halfbinary.scavengerhuntapi.model.HuntId import net.halfbinary.scavengerhuntapi.model.HuntId
import net.halfbinary.scavengerhuntapi.model.HunterId 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.request.HuntStatus
import net.halfbinary.scavengerhuntapi.model.response.HuntResponse import net.halfbinary.scavengerhuntapi.model.response.HuntResponse
import net.halfbinary.scavengerhuntapi.service.HuntService import net.halfbinary.scavengerhuntapi.service.HuntService
import net.halfbinary.scavengerhuntapi.service.HunterService
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.security.access.prepost.PreAuthorize import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.security.core.Authentication
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
import java.time.LocalDateTime
@RestController @RestController
@RequestMapping("hunt") @RequestMapping("hunt")
class HuntController(private val huntService: HuntService) { class HuntController(private val huntService: HuntService, private val hunterService: HunterService) {
@GetMapping("/{id}") @GetMapping("/{id}")
@Operation(summary = "Gets the specified hunt information")
fun getHunt(@PathVariable("id") huntId: HuntId): ResponseEntity<HuntResponse> { fun getHunt(@PathVariable("id") huntId: HuntId): ResponseEntity<HuntResponse> {
return ResponseEntity.ok(huntService.getHunt(huntId).toResponse()) return ResponseEntity.ok(huntService.getHunt(huntId).toResponse())
} }
@PreAuthorize("hasRole('ADMIN')") @PreAuthorize("hasRole('ADMIN')")
@Tag(name = "Admin")
@GetMapping() @GetMapping()
@Operation(summary = "Gets all Hunts")
fun getAllHunts(@RequestParam status: HuntStatus?): ResponseEntity<List<HuntResponse>> { fun getAllHunts(@RequestParam status: HuntStatus?): ResponseEntity<List<HuntResponse>> {
return ResponseEntity.ok(huntService.getAllHunts(status).map { it.toResponse() }) 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() @PostMapping()
@Operation(summary = "Creates a new Hunt")
fun createHunt(@Valid @RequestBody huntRequest: HuntCreateRequest): ResponseEntity<HuntResponse> { fun createHunt(@Valid @RequestBody huntRequest: HuntCreateRequest): ResponseEntity<HuntResponse> {
return ResponseEntity.ok(huntService.createHunt(huntRequest.toDomain()).toResponse()) return ResponseEntity.ok(huntService.createHunt(huntRequest.toDomain()).toResponse())
} }
@PreAuthorize("hasRole('ADMIN')")
@Tag(name = "Admin")
@GetMapping("/hunter/{hunterId}") @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() }) 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 package net.halfbinary.scavengerhuntapi.controller
import io.swagger.v3.oas.annotations.Operation
import jakarta.validation.Valid import jakarta.validation.Valid
import net.halfbinary.scavengerhuntapi.model.HuntId 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.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.model.response.TeamResponse
import net.halfbinary.scavengerhuntapi.service.TeamService
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.security.core.Authentication
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping 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 import org.springframework.web.bind.annotation.RestController
@RestController @RestController
@RequestMapping("hunt/{id}/team") @RequestMapping("hunt/{huntId}/team")
class TeamController { class TeamController(private val teamService: TeamService) {
@GetMapping @GetMapping
fun listHuntTeams(@PathVariable id: HuntId): ResponseEntity<List<TeamResponse>> { @Operation(summary = "List all teams for the specified hunt")
TODO() fun listHuntTeams(@PathVariable huntId: HuntId): ResponseEntity<List<TeamResponse>> {
return ResponseEntity.ok(teamService.getListOfTeamsForHunt(huntId).map { it.toResponse()})
} }
@PostMapping @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() 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 package net.halfbinary.scavengerhuntapi.model
enum class FoundStatus { enum class FoundStatus {
NOT_FOUND,
SUBMITTED, SUBMITTED,
APPROVED, APPROVED,
REJECTED, REJECTED,

View File

@@ -7,4 +7,6 @@ typealias HuntId = UUID
typealias HunterId = UUID typealias HunterId = UUID
typealias ItemId = UUID typealias ItemId = UUID
typealias TeamId = 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.Id
import jakarta.persistence.Table import jakarta.persistence.Table
import net.halfbinary.scavengerhuntapi.model.HuntId import net.halfbinary.scavengerhuntapi.model.HuntId
import net.halfbinary.scavengerhuntapi.model.TeamHuntId
import net.halfbinary.scavengerhuntapi.model.TeamId import net.halfbinary.scavengerhuntapi.model.TeamId
import java.util.* import java.util.*
@@ -11,7 +12,7 @@ import java.util.*
@Table(name = "team_hunt") @Table(name = "team_hunt")
data class TeamHuntRecord( data class TeamHuntRecord(
@Id @Id
val id: UUID, val id: TeamHuntId,
val teamId: TeamId, val teamId: TeamId,
val huntId: HuntId 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.HuntId
import net.halfbinary.scavengerhuntapi.model.HunterId import net.halfbinary.scavengerhuntapi.model.HunterId
import net.halfbinary.scavengerhuntapi.model.TeamId
import net.halfbinary.scavengerhuntapi.model.record.HuntRecord import net.halfbinary.scavengerhuntapi.model.record.HuntRecord
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query import org.springframework.data.jpa.repository.Query
@@ -23,6 +24,17 @@ interface HuntRepository : JpaRepository<HuntRecord, HuntId> {
""", nativeQuery = true) """, nativeQuery = true)
fun findAllOngoingByHunter(hunterId: HunterId): List<HuntRecord> 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(""" @Query("""
SELECT h.* SELECT h.*
FROM hunt 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 package net.halfbinary.scavengerhuntapi.repository
import net.halfbinary.scavengerhuntapi.model.HuntId
import net.halfbinary.scavengerhuntapi.model.TeamId import net.halfbinary.scavengerhuntapi.model.TeamId
import net.halfbinary.scavengerhuntapi.model.record.TeamRecord import net.halfbinary.scavengerhuntapi.model.record.TeamRecord
import org.springframework.data.jpa.repository.JpaRepository 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 net.halfbinary.scavengerhuntapi.repository.HuntRepository
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.time.LocalDateTime
@Service @Service
class HuntService(private val huntRepository: HuntRepository) { class HuntService(private val huntRepository: HuntRepository) {
@@ -30,6 +31,27 @@ class HuntService(private val huntRepository: HuntRepository) {
return huntRepository.findAllOngoingByHunter(hunterId).map { it.toDomain() } 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 { fun createHunt(hunt: Hunt): Hunt {
return huntRepository.save(hunt.toRecord()).toDomain() 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 package net.halfbinary.scavengerhuntapi.service
import net.halfbinary.scavengerhuntapi.error.exception.NotFoundException
import net.halfbinary.scavengerhuntapi.model.HuntId import net.halfbinary.scavengerhuntapi.model.HuntId
import net.halfbinary.scavengerhuntapi.model.TeamId 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.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 org.springframework.stereotype.Service
import java.util.UUID
@Service @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> { fun getListOfTeamsForHunt(huntId: HuntId): List<Team> {
TODO() return getTeamsForHunt(huntId)
} }
fun createTeam(name: String): Team { fun createTeam(name: String): Team {
TODO() return teamRepository.save(TeamRequest(name).toDomain().toRecord()).toDomain()
} }
fun addTeamToHunt(huntId: HuntId, teamId: TeamId) { 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.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.url=${DB_URL}
spring.datasource.username=${DB_USER} spring.datasource.username=${DB_USER}
spring.datasource.password=${DB_PASSWORD} spring.datasource.password=${DB_PASSWORD}
jwt.secret=${JWT_SECRET} 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