Compare commits
8 Commits
14d98f20a3
...
feature/hu
| Author | SHA1 | Date | |
|---|---|---|---|
| 457021aeec | |||
| 5905882763 | |||
| 04b61485ea | |||
| 302feeab1e | |||
| 8b808bfd34 | |||
| 78489010be | |||
| 5563c0c774 | |||
| 29802d8b43 |
@@ -3,6 +3,7 @@ plugins {
|
|||||||
kotlin("plugin.spring") version "2.2.21"
|
kotlin("plugin.spring") version "2.2.21"
|
||||||
id("org.springframework.boot") version "4.0.0"
|
id("org.springframework.boot") version "4.0.0"
|
||||||
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"
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "net.halfbinary"
|
group = "net.halfbinary"
|
||||||
@@ -26,16 +27,22 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
val mysqlConnectorJ = "9.5.0"
|
||||||
|
val commonsValidator = "1.10.1"
|
||||||
|
val jakartaValidation = "3.1.1"
|
||||||
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-webmvc")
|
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("jakarta.validation:jakarta.validation-api:${jakartaValidation}")
|
||||||
|
implementation("com.mysql:mysql-connector-j:${mysqlConnectorJ}")
|
||||||
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}")
|
||||||
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
||||||
runtimeOnly("com.h2database:h2")
|
|
||||||
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")
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-webmvc-test")
|
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
|
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
|
||||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
}
|
}
|
||||||
@@ -46,6 +53,12 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
allOpen {
|
||||||
|
annotation("jakarta.persistence.Entity")
|
||||||
|
annotation("jakarta.persistence.MappedSuperclass")
|
||||||
|
annotation("jakarta.persistence.Embeddable")
|
||||||
|
}
|
||||||
|
|
||||||
tasks.withType<Test> {
|
tasks.withType<Test> {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.controller
|
||||||
|
|
||||||
|
import jakarta.validation.Valid
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.HuntId
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.HunterId
|
||||||
|
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.response.HuntResponse
|
||||||
|
import net.halfbinary.scavengerhuntapi.service.HuntService
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.web.bind.annotation.*
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("hunt")
|
||||||
|
class HuntController(private val huntService: HuntService) {
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
fun getHunt(@PathVariable("id") huntId: HuntId): ResponseEntity<HuntResponse> {
|
||||||
|
return ResponseEntity.ok(huntService.getHunt(huntId).toResponse())
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping()
|
||||||
|
fun getAllHunts(@RequestParam status: HuntStatus?): ResponseEntity<List<HuntResponse>> {
|
||||||
|
return ResponseEntity.ok(huntService.getAllHunts(status).map { it.toResponse() })
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping()
|
||||||
|
fun createHunt(@Valid @RequestBody huntRequest: HuntCreateRequest): ResponseEntity<HuntResponse> {
|
||||||
|
return ResponseEntity.ok(huntService.createHunt(huntRequest.toDomain()).toResponse())
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/hunter/{hunterId}")
|
||||||
|
fun getHuntsByHunter(@PathVariable("hunterId") hunterId: HunterId): ResponseEntity<List<HuntResponse>> {
|
||||||
|
return ResponseEntity.ok(huntService.getHuntsByHunter(hunterId).map { it.toResponse() })
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.controller
|
||||||
|
|
||||||
|
import jakarta.servlet.http.Cookie
|
||||||
|
import jakarta.servlet.http.HttpServletResponse
|
||||||
|
import jakarta.validation.Valid
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.converter.toDomain
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.converter.toLoginResponse
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.request.LoginRequest
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.response.LoginResponse
|
||||||
|
import net.halfbinary.scavengerhuntapi.service.LoginService
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
import java.net.URLEncoder
|
||||||
|
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
class LoginController(private val loginService: LoginService) {
|
||||||
|
@PostMapping("/login")
|
||||||
|
fun login(@Valid @RequestBody body: LoginRequest, response: HttpServletResponse): ResponseEntity<LoginResponse> {
|
||||||
|
val result = loginService.login(body.toDomain())
|
||||||
|
val creds = "${result.email}|${result.name}"
|
||||||
|
val encodedCreds = URLEncoder.encode(creds, "UTF-8")
|
||||||
|
response.addCookie(Cookie("creds", encodedCreds))
|
||||||
|
return ResponseEntity.ok(result.toLoginResponse())
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/logout")
|
||||||
|
fun logout(response: HttpServletResponse): ResponseEntity<String> {
|
||||||
|
val cookie = Cookie("creds", null)
|
||||||
|
cookie.maxAge = 0
|
||||||
|
response.addCookie(cookie)
|
||||||
|
return ResponseEntity.ok("OK")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.controller
|
||||||
|
|
||||||
|
import jakarta.validation.Valid
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.converter.toDomain
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.request.HunterSignupRequest
|
||||||
|
import net.halfbinary.scavengerhuntapi.service.SignupService
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
class SignupController(private val signupService: SignupService) {
|
||||||
|
@PostMapping("/signup")
|
||||||
|
fun hunterSignup(@Valid @RequestBody body: HunterSignupRequest): ResponseEntity<Any> {
|
||||||
|
signupService.createNewHunter(body.toDomain())
|
||||||
|
return ResponseEntity.ok().build()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.error
|
||||||
|
|
||||||
|
import net.halfbinary.scavengerhuntapi.error.exception.InvalidEmailException
|
||||||
|
import net.halfbinary.scavengerhuntapi.error.exception.LoginFailedException
|
||||||
|
import net.halfbinary.scavengerhuntapi.error.exception.NotFoundException
|
||||||
|
import net.halfbinary.scavengerhuntapi.error.exception.PreexistingAccountException
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.http.converter.HttpMessageNotReadableException
|
||||||
|
import org.springframework.validation.FieldError
|
||||||
|
import org.springframework.web.bind.MethodArgumentNotValidException
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||||
|
import org.springframework.web.bind.annotation.ResponseStatus
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice
|
||||||
|
|
||||||
|
|
||||||
|
@RestControllerAdvice
|
||||||
|
class ExceptionHandler {
|
||||||
|
|
||||||
|
@ExceptionHandler(PreexistingAccountException::class)
|
||||||
|
@ResponseStatus(HttpStatus.CONFLICT)
|
||||||
|
fun preexistingAccountException(e: PreexistingAccountException): String? {
|
||||||
|
return e.message
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(LoginFailedException::class)
|
||||||
|
@ResponseStatus(HttpStatus.UNAUTHORIZED)
|
||||||
|
fun loginFailedException(e: LoginFailedException): String? {
|
||||||
|
return e.message
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(InvalidEmailException::class)
|
||||||
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
|
fun invalidEmailException(e: InvalidEmailException): String? {
|
||||||
|
return e.message
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(NotFoundException::class)
|
||||||
|
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||||
|
fun notFoundException(e: NotFoundException): String? {
|
||||||
|
return e.message
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(HttpMessageNotReadableException::class)
|
||||||
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
|
fun httpMessageNotReadableException(e: HttpMessageNotReadableException): String? {
|
||||||
|
return e.message
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(MethodArgumentNotValidException::class)
|
||||||
|
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||||
|
fun handleValidationExceptions(e: MethodArgumentNotValidException): Map<String, String?> {
|
||||||
|
return e.bindingResult.allErrors.associate { error ->
|
||||||
|
Pair(
|
||||||
|
(error as FieldError).field,
|
||||||
|
error.defaultMessage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.error.exception
|
||||||
|
|
||||||
|
class InvalidEmailException(email: String): RuntimeException("The email ${email} is not valid.")
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.error.exception
|
||||||
|
|
||||||
|
class LoginFailedException(): RuntimeException("The email and password combination is not correct.")
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.error.exception
|
||||||
|
|
||||||
|
class NotFoundException(override val message: String): RuntimeException(message)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.error.exception
|
||||||
|
|
||||||
|
class PreexistingAccountException: RuntimeException("An account with that email already exists.")
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.model
|
||||||
|
|
||||||
|
enum class FoundStatus {
|
||||||
|
SUBMITTED,
|
||||||
|
APPROVED,
|
||||||
|
REJECTED,
|
||||||
|
REMOVED
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.model
|
||||||
|
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
typealias FoundId = UUID
|
||||||
|
typealias HuntId = UUID
|
||||||
|
typealias HunterId = UUID
|
||||||
|
typealias ItemId = UUID
|
||||||
|
typealias TeamId = UUID
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.model.converter
|
||||||
|
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.domain.Hunt
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.record.HuntRecord
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.request.HuntCreateRequest
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.response.HuntResponse
|
||||||
|
|
||||||
|
fun HuntRecord.toDomain(): Hunt {
|
||||||
|
return Hunt(id, title, startDateTime, endDateTime, isTerminated)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Hunt.toResponse(): HuntResponse {
|
||||||
|
return HuntResponse(id, title, startDateTime, endDateTime, isTerminated)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun HuntCreateRequest.toDomain(): Hunt {
|
||||||
|
return Hunt(title = title, startDateTime = startDateTime, endDateTime = endDateTime, isTerminated = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Hunt.toRecord(): HuntRecord {
|
||||||
|
return HuntRecord(id, title, startDateTime, endDateTime, isTerminated)
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.model.converter
|
||||||
|
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.domain.Hunter
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.record.HunterRecord
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.request.HunterSignupRequest
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.response.LoginResponse
|
||||||
|
|
||||||
|
fun HunterSignupRequest.toDomain(): Hunter {
|
||||||
|
return Hunter(
|
||||||
|
email = email,
|
||||||
|
name = name,
|
||||||
|
password = password,
|
||||||
|
isAdmin = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Hunter.toRecord(): HunterRecord {
|
||||||
|
return HunterRecord(id, email, name, password, isAdmin)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun HunterRecord.toDomain(): Hunter {
|
||||||
|
return Hunter(id, email, name, password, isAdmin)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Hunter.toLoginResponse(): LoginResponse {
|
||||||
|
return LoginResponse(email, name)
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.model.converter
|
||||||
|
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.domain.Login
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.request.LoginRequest
|
||||||
|
|
||||||
|
fun LoginRequest.toDomain(): Login {
|
||||||
|
return Login(email, password)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.model.domain
|
||||||
|
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.HuntId
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
data class Hunt(
|
||||||
|
val id: HuntId = UUID.randomUUID(),
|
||||||
|
val title: String,
|
||||||
|
val startDateTime: LocalDateTime,
|
||||||
|
val endDateTime: LocalDateTime,
|
||||||
|
val isTerminated: Boolean
|
||||||
|
)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.model.domain
|
||||||
|
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.HunterId
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
data class Hunter(
|
||||||
|
val id: HunterId = UUID.randomUUID(),
|
||||||
|
val email: String,
|
||||||
|
val name: String,
|
||||||
|
val password: String,
|
||||||
|
val isAdmin: Boolean
|
||||||
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.model.domain
|
||||||
|
|
||||||
|
data class Login(
|
||||||
|
val email: String,
|
||||||
|
val password: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.model.record
|
||||||
|
|
||||||
|
import jakarta.persistence.Entity
|
||||||
|
import jakarta.persistence.Id
|
||||||
|
import jakarta.persistence.Table
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.*
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a found Item for a Hunt by a Hunter
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "found")
|
||||||
|
data class FoundRecord(
|
||||||
|
@Id
|
||||||
|
val id: FoundId,
|
||||||
|
val itemId: ItemId,
|
||||||
|
val huntId: HuntId,
|
||||||
|
val hunterId: HunterId,
|
||||||
|
val foundDateTime: LocalDateTime,
|
||||||
|
val imageName: String,
|
||||||
|
val status: FoundStatus
|
||||||
|
)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.model.record
|
||||||
|
|
||||||
|
import jakarta.persistence.Entity
|
||||||
|
import jakarta.persistence.Id
|
||||||
|
import jakarta.persistence.Table
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.HuntId
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.ItemId
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "hunt_item")
|
||||||
|
data class HuntItemRecord(
|
||||||
|
@Id
|
||||||
|
val id: UUID,
|
||||||
|
val huntId: HuntId,
|
||||||
|
val itemId: ItemId
|
||||||
|
)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.model.record
|
||||||
|
|
||||||
|
import jakarta.persistence.Entity
|
||||||
|
import jakarta.persistence.Id
|
||||||
|
import jakarta.persistence.Table
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.HuntId
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a scavenger hunt event
|
||||||
|
* @property isTerminated Is the event prematurely stopped
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "hunt")
|
||||||
|
data class HuntRecord(
|
||||||
|
@Id
|
||||||
|
val id: HuntId,
|
||||||
|
val title: String,
|
||||||
|
val startDateTime: LocalDateTime,
|
||||||
|
val endDateTime: LocalDateTime,
|
||||||
|
val isTerminated: Boolean
|
||||||
|
)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.model.record
|
||||||
|
|
||||||
|
import jakarta.persistence.Entity
|
||||||
|
import jakarta.persistence.Id
|
||||||
|
import jakarta.persistence.Table
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.HuntId
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.HunterId
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connects a Hunter to a Hunt as a judge
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "hunter_hunt")
|
||||||
|
data class HunterHuntRecord(
|
||||||
|
@Id
|
||||||
|
val id: UUID,
|
||||||
|
val hunterId: HunterId,
|
||||||
|
val huntId: HuntId
|
||||||
|
)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.model.record
|
||||||
|
|
||||||
|
import jakarta.persistence.Entity
|
||||||
|
import jakarta.persistence.Id
|
||||||
|
import jakarta.persistence.Table
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.HunterId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a user
|
||||||
|
* @property isAdmin Is a site administrator
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "hunter")
|
||||||
|
data class HunterRecord(
|
||||||
|
@Id
|
||||||
|
val id: HunterId,
|
||||||
|
val email: String,
|
||||||
|
val name: String,
|
||||||
|
val password: String,
|
||||||
|
val isAdmin: Boolean
|
||||||
|
)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.model.record
|
||||||
|
|
||||||
|
import jakarta.persistence.Entity
|
||||||
|
import jakarta.persistence.Id
|
||||||
|
import jakarta.persistence.Table
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.HunterId
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.TeamId
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "hunter_team")
|
||||||
|
data class HunterTeamRecord(
|
||||||
|
@Id
|
||||||
|
val id: UUID,
|
||||||
|
val hunterId: HunterId,
|
||||||
|
val teamId: TeamId
|
||||||
|
)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.model.record
|
||||||
|
|
||||||
|
import jakarta.persistence.Entity
|
||||||
|
import jakarta.persistence.Id
|
||||||
|
import jakarta.persistence.Table
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.ItemId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an item to be found on a Hunt
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "item")
|
||||||
|
data class ItemRecord(
|
||||||
|
@Id
|
||||||
|
val id: ItemId,
|
||||||
|
val name: String,
|
||||||
|
val points: Int
|
||||||
|
)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.model.record
|
||||||
|
|
||||||
|
import jakarta.persistence.Entity
|
||||||
|
import jakarta.persistence.Id
|
||||||
|
import jakarta.persistence.Table
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.HuntId
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.TeamId
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "team_hunt")
|
||||||
|
data class TeamHuntRecord(
|
||||||
|
@Id
|
||||||
|
val id: UUID,
|
||||||
|
val teamId: TeamId,
|
||||||
|
val huntId: HuntId
|
||||||
|
)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.model.record
|
||||||
|
|
||||||
|
import jakarta.persistence.Entity
|
||||||
|
import jakarta.persistence.Id
|
||||||
|
import jakarta.persistence.Table
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.TeamId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a group (1+) of people hunting on a Hunt
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "team")
|
||||||
|
data class TeamRecord(
|
||||||
|
@Id
|
||||||
|
val id: TeamId,
|
||||||
|
val name: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.model.request
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Future
|
||||||
|
import jakarta.validation.constraints.NotBlank
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
data class HuntCreateRequest(
|
||||||
|
@field:NotBlank(message = "Hunt title is required")
|
||||||
|
val title: String,
|
||||||
|
@field:Future
|
||||||
|
val startDateTime: LocalDateTime,
|
||||||
|
@field:Future
|
||||||
|
val endDateTime: LocalDateTime,
|
||||||
|
)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.model.request
|
||||||
|
|
||||||
|
enum class HuntStatus {
|
||||||
|
UNSTARTED,
|
||||||
|
ONGOING,
|
||||||
|
CLOSED
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.model.request
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Email
|
||||||
|
import jakarta.validation.constraints.NotBlank
|
||||||
|
|
||||||
|
data class HunterSignupRequest(
|
||||||
|
@field:Email(message = "Must be a valid email address")
|
||||||
|
@field:NotBlank(message = "Email must not be blank")
|
||||||
|
val email: String,
|
||||||
|
@field:NotBlank(message = "Name cannot be blank")
|
||||||
|
val name: String,
|
||||||
|
@field:NotBlank(message = "Password cannot be blank")
|
||||||
|
val password: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.model.request
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank
|
||||||
|
|
||||||
|
data class LoginRequest(
|
||||||
|
@field:NotBlank(message = "Email cannot be blank")
|
||||||
|
val email: String,
|
||||||
|
@field:NotBlank(message = "Password cannot be blank")
|
||||||
|
val password: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.model.response
|
||||||
|
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.HuntId
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
data class HuntResponse(
|
||||||
|
val id: HuntId,
|
||||||
|
val title: String,
|
||||||
|
val startDateTime: LocalDateTime,
|
||||||
|
val endDateTime: LocalDateTime,
|
||||||
|
val isTerminated: Boolean
|
||||||
|
)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.model.response
|
||||||
|
|
||||||
|
data class LoginResponse(
|
||||||
|
val email: String,
|
||||||
|
val name: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.repository
|
||||||
|
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.FoundId
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.record.FoundRecord
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
interface FoundRepository : JpaRepository<FoundRecord, FoundId>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.repository
|
||||||
|
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.HuntId
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.HunterId
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.record.HuntRecord
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
import org.springframework.data.jpa.repository.Query
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
interface HuntRepository : JpaRepository<HuntRecord, HuntId> {
|
||||||
|
@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.id = :hunterId
|
||||||
|
AND h.is_terminated = FALSE
|
||||||
|
AND h.start_date_time < NOW()
|
||||||
|
AND h.end_date_time > NOW()
|
||||||
|
""", nativeQuery = true)
|
||||||
|
fun findAllOngoingByHunter(hunterId: HunterId): List<HuntRecord>
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT h.*
|
||||||
|
FROM hunt h
|
||||||
|
WHERE h.is_terminated = FALSE
|
||||||
|
AND h.start_date_time < NOW()
|
||||||
|
AND h.end_date_time > NOW()
|
||||||
|
""", nativeQuery = true)
|
||||||
|
fun findAllOngoing(): List<HuntRecord>
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT h.*
|
||||||
|
FROM hunt h
|
||||||
|
WHERE h.is_terminated = FALSE
|
||||||
|
AND h.start_date_time > NOW()
|
||||||
|
""", nativeQuery = true)
|
||||||
|
fun findAllUnstarted(): List<HuntRecord>
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT h.*
|
||||||
|
FROM hunt h
|
||||||
|
WHERE h.is_terminated = TRUE
|
||||||
|
""", nativeQuery = true)
|
||||||
|
fun findAllClosed(): List<HuntRecord>
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.repository
|
||||||
|
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.HunterId
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.record.HunterRecord
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
import org.springframework.data.jpa.repository.Query
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
interface HunterRepository : JpaRepository<HunterRecord, HunterId> {
|
||||||
|
fun findByEmail(email: String): HunterRecord?
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
SELECT h.*
|
||||||
|
FROM hunter h
|
||||||
|
WHERE h.email = :email
|
||||||
|
AND h.password = :password
|
||||||
|
""", nativeQuery = true)
|
||||||
|
fun login(email: String, password: String): HunterRecord?
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.repository
|
||||||
|
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.ItemId
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.record.ItemRecord
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
interface ItemRepository : JpaRepository<ItemRecord, ItemId>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.repository
|
||||||
|
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.TeamId
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.record.TeamRecord
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
interface TeamRepository : JpaRepository<TeamRecord, TeamId>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.service
|
||||||
|
|
||||||
|
import net.halfbinary.scavengerhuntapi.error.exception.NotFoundException
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.HuntId
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.HunterId
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.converter.toDomain
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.converter.toRecord
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.domain.Hunt
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.request.HuntStatus
|
||||||
|
import net.halfbinary.scavengerhuntapi.repository.HuntRepository
|
||||||
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class HuntService(private val huntRepository: HuntRepository) {
|
||||||
|
fun getHunt(huntId: HuntId): Hunt {
|
||||||
|
return huntRepository.findByIdOrNull(huntId)?.toDomain() ?: throw NotFoundException("No hunt with id ${huntId} found")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAllHunts(status: HuntStatus?): List<Hunt> {
|
||||||
|
return when(status) {
|
||||||
|
HuntStatus.UNSTARTED -> huntRepository.findAllUnstarted().map { it.toDomain() }
|
||||||
|
HuntStatus.ONGOING -> huntRepository.findAllOngoing().map { it.toDomain() }
|
||||||
|
HuntStatus.CLOSED -> huntRepository.findAllClosed().map { it.toDomain() }
|
||||||
|
else -> huntRepository.findAll().map { it.toDomain() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getHuntsByHunter(hunterId: HunterId): List<Hunt> {
|
||||||
|
return huntRepository.findAllOngoingByHunter(hunterId).map { it.toDomain() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createHunt(hunt: Hunt): Hunt {
|
||||||
|
return huntRepository.save(hunt.toRecord()).toDomain()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.service
|
||||||
|
|
||||||
|
import net.halfbinary.scavengerhuntapi.error.exception.LoginFailedException
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.converter.toDomain
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.domain.Hunter
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.domain.Login
|
||||||
|
import net.halfbinary.scavengerhuntapi.repository.HunterRepository
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class LoginService(private val hunterRepository: HunterRepository) {
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(LoginService::class.java)
|
||||||
|
}
|
||||||
|
fun login(login: Login): Hunter {
|
||||||
|
log.info("Logging in with email: ${login.email}")
|
||||||
|
return hunterRepository.login(login.email, login.password)?.toDomain()?:throw LoginFailedException()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package net.halfbinary.scavengerhuntapi.service
|
||||||
|
|
||||||
|
import net.halfbinary.scavengerhuntapi.error.exception.InvalidEmailException
|
||||||
|
import net.halfbinary.scavengerhuntapi.error.exception.PreexistingAccountException
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.converter.toRecord
|
||||||
|
import net.halfbinary.scavengerhuntapi.model.domain.Hunter
|
||||||
|
import net.halfbinary.scavengerhuntapi.repository.HunterRepository
|
||||||
|
import org.apache.commons.validator.routines.EmailValidator
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class SignupService(private val hunterRepository: HunterRepository) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(SignupService::class.java)
|
||||||
|
}
|
||||||
|
fun createNewHunter(hunter: Hunter) {
|
||||||
|
log.info("Creating new Hunter with email: ${hunter.email}...")
|
||||||
|
if (!EmailValidator.getInstance().isValid(hunter.email)) {
|
||||||
|
log.error("Invalid email ${hunter.email}")
|
||||||
|
throw InvalidEmailException(hunter.email)
|
||||||
|
}
|
||||||
|
if (hunterRepository.findByEmail(hunter.email) != null) {
|
||||||
|
log.error("Hunter ${hunter.email} already exists")
|
||||||
|
throw PreexistingAccountException()
|
||||||
|
}
|
||||||
|
hunterRepository.save(hunter.toRecord())
|
||||||
|
log.info("...Created new Hunter with email: ${hunter.email}")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,10 @@
|
|||||||
spring.application.name=scavengerhuntapi
|
spring.application.name=scavengerhuntapi
|
||||||
|
|
||||||
|
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.url=${DB_URL}
|
||||||
|
spring.datasource.username=${DB_USER}
|
||||||
|
spring.datasource.password=${DB_PASSWORD}
|
||||||
11
src/main/resources/logback.xml
Normal file
11
src/main/resources/logback.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<configuration>
|
||||||
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
<logger name="net.halfbinary.scavengerhuntapi" level="debug" />
|
||||||
|
<root level="info">
|
||||||
|
<appender-ref ref="STDOUT" />
|
||||||
|
</root>
|
||||||
|
</configuration>
|
||||||
Reference in New Issue
Block a user