Adds login ability, error handling, and logging (#1)

Reviewed-on: #1
Co-authored-by: aarbit <aarbit@gmail.com>
Co-committed-by: aarbit <aarbit@gmail.com>
This commit was merged in pull request #1.
This commit is contained in:
2025-12-19 05:15:18 +00:00
committed by aarbit
parent 302feeab1e
commit 04b61485ea
13 changed files with 155 additions and 8 deletions

View File

@@ -0,0 +1,35 @@
package net.halfbinary.scavengerhuntapi.controller
import jakarta.servlet.http.Cookie
import jakarta.servlet.http.HttpServletResponse
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(@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

@@ -12,12 +12,7 @@ import org.springframework.web.bind.annotation.RestController
class SignupController(private val signupService: SignupService) {
@PostMapping("/signup")
fun hunterSignup(@RequestBody body: HunterSignupRequest): ResponseEntity<Any> {
try {
signupService.createNewHunter(body.toDomain())
return ResponseEntity.ok().build()
} catch (e: RuntimeException) {
return ResponseEntity.badRequest().body(e.message)
}
signupService.createNewHunter(body.toDomain())
return ResponseEntity.ok().build()
}
}

View File

@@ -0,0 +1,31 @@
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.PreexistingAccountException
import org.springframework.http.HttpStatus
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
}
}

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

@@ -3,6 +3,7 @@ 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(
@@ -15,4 +16,12 @@ fun HunterSignupRequest.toDomain(): Hunter {
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,6 @@
package net.halfbinary.scavengerhuntapi.model.domain
data class Login(
val email: String,
val password: String
)

View File

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

View File

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

View File

@@ -3,10 +3,18 @@ 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>
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,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

@@ -6,17 +6,26 @@ 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

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