8 Commits

42 changed files with 724 additions and 3 deletions

View File

@@ -3,6 +3,7 @@ plugins {
kotlin("plugin.spring") version "2.2.21"
id("org.springframework.boot") version "4.0.0"
id("io.spring.dependency-management") version "1.1.7"
kotlin("plugin.jpa") version "2.2.21"
}
group = "net.halfbinary"
@@ -26,16 +27,22 @@ repositories {
}
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-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-validation")
implementation("jakarta.validation:jakarta.validation-api:${jakartaValidation}")
implementation("com.mysql:mysql-connector-j:${mysqlConnectorJ}")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("commons-validator:commons-validator:${commonsValidator}")
developmentOnly("org.springframework.boot:spring-boot-devtools")
runtimeOnly("com.h2database:h2")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
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")
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> {
useJUnitPlatform()
}

View File

@@ -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() })
}
}

View File

@@ -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")
}
}

View File

@@ -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()
}
}

View File

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

View File

@@ -0,0 +1,3 @@
package net.halfbinary.scavengerhuntapi.error.exception
class InvalidEmailException(email: String): RuntimeException("The email ${email} is not valid.")

View File

@@ -0,0 +1,3 @@
package net.halfbinary.scavengerhuntapi.error.exception
class LoginFailedException(): RuntimeException("The email and password combination is not correct.")

View File

@@ -0,0 +1,3 @@
package net.halfbinary.scavengerhuntapi.error.exception
class NotFoundException(override val message: String): RuntimeException(message)

View File

@@ -0,0 +1,3 @@
package net.halfbinary.scavengerhuntapi.error.exception
class PreexistingAccountException: RuntimeException("An account with that email already exists.")

View File

@@ -0,0 +1,8 @@
package net.halfbinary.scavengerhuntapi.model
enum class FoundStatus {
SUBMITTED,
APPROVED,
REJECTED,
REMOVED
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -0,0 +1,6 @@
package net.halfbinary.scavengerhuntapi.model.domain
data class Login(
val email: String,
val password: String
)

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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,
)

View File

@@ -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,
)

View File

@@ -0,0 +1,7 @@
package net.halfbinary.scavengerhuntapi.model.request
enum class HuntStatus {
UNSTARTED,
ONGOING,
CLOSED
}

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -0,0 +1,6 @@
package net.halfbinary.scavengerhuntapi.model.response
data class LoginResponse(
val email: String,
val name: String
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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}")
}
}

View File

@@ -1 +1,10 @@
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}

View 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>