From 1ff6532ada4ef86e8f15c3bc9880bc3bc0da073f Mon Sep 17 00:00:00 2001 From: aarbit Date: Thu, 18 Dec 2025 23:12:16 -0600 Subject: [PATCH] Adds login ability, error handling, and logging --- .../controller/LoginController.kt | 35 +++++++++++++++++++ .../controller/SignupController.kt | 9 ++--- .../error/ExceptionHandler.kt | 31 ++++++++++++++++ .../error/exception/LoginFailedException.kt | 3 ++ .../model/converter/HunterConverter.kt | 9 +++++ .../model/converter/LoginConverter.kt | 8 +++++ .../scavengerhuntapi/model/domain/Login.kt | 6 ++++ .../model/request/LoginRequest.kt | 6 ++++ .../model/response/LoginResponse.kt | 6 ++++ .../repository/HunterRepository.kt | 10 +++++- .../scavengerhuntapi/service/LoginService.kt | 20 +++++++++++ .../scavengerhuntapi/service/SignupService.kt | 9 +++++ src/main/resources/logback.xml | 11 ++++++ 13 files changed, 155 insertions(+), 8 deletions(-) create mode 100644 src/main/kotlin/net/halfbinary/scavengerhuntapi/controller/LoginController.kt create mode 100644 src/main/kotlin/net/halfbinary/scavengerhuntapi/error/ExceptionHandler.kt create mode 100644 src/main/kotlin/net/halfbinary/scavengerhuntapi/error/exception/LoginFailedException.kt create mode 100644 src/main/kotlin/net/halfbinary/scavengerhuntapi/model/converter/LoginConverter.kt create mode 100644 src/main/kotlin/net/halfbinary/scavengerhuntapi/model/domain/Login.kt create mode 100644 src/main/kotlin/net/halfbinary/scavengerhuntapi/model/request/LoginRequest.kt create mode 100644 src/main/kotlin/net/halfbinary/scavengerhuntapi/model/response/LoginResponse.kt create mode 100644 src/main/kotlin/net/halfbinary/scavengerhuntapi/service/LoginService.kt create mode 100644 src/main/resources/logback.xml diff --git a/src/main/kotlin/net/halfbinary/scavengerhuntapi/controller/LoginController.kt b/src/main/kotlin/net/halfbinary/scavengerhuntapi/controller/LoginController.kt new file mode 100644 index 0000000..3d623a7 --- /dev/null +++ b/src/main/kotlin/net/halfbinary/scavengerhuntapi/controller/LoginController.kt @@ -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 { + 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 { + val cookie = Cookie("creds", null) + cookie.maxAge = 0 + response.addCookie(cookie) + return ResponseEntity.ok("OK") + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/halfbinary/scavengerhuntapi/controller/SignupController.kt b/src/main/kotlin/net/halfbinary/scavengerhuntapi/controller/SignupController.kt index 496d62e..2fee3f0 100644 --- a/src/main/kotlin/net/halfbinary/scavengerhuntapi/controller/SignupController.kt +++ b/src/main/kotlin/net/halfbinary/scavengerhuntapi/controller/SignupController.kt @@ -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 { - 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() } } \ No newline at end of file diff --git a/src/main/kotlin/net/halfbinary/scavengerhuntapi/error/ExceptionHandler.kt b/src/main/kotlin/net/halfbinary/scavengerhuntapi/error/ExceptionHandler.kt new file mode 100644 index 0000000..33883a2 --- /dev/null +++ b/src/main/kotlin/net/halfbinary/scavengerhuntapi/error/ExceptionHandler.kt @@ -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 + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/halfbinary/scavengerhuntapi/error/exception/LoginFailedException.kt b/src/main/kotlin/net/halfbinary/scavengerhuntapi/error/exception/LoginFailedException.kt new file mode 100644 index 0000000..08e6447 --- /dev/null +++ b/src/main/kotlin/net/halfbinary/scavengerhuntapi/error/exception/LoginFailedException.kt @@ -0,0 +1,3 @@ +package net.halfbinary.scavengerhuntapi.error.exception + +class LoginFailedException(): RuntimeException("The email and password combination is not correct.") \ No newline at end of file diff --git a/src/main/kotlin/net/halfbinary/scavengerhuntapi/model/converter/HunterConverter.kt b/src/main/kotlin/net/halfbinary/scavengerhuntapi/model/converter/HunterConverter.kt index 202e28d..eb189b2 100644 --- a/src/main/kotlin/net/halfbinary/scavengerhuntapi/model/converter/HunterConverter.kt +++ b/src/main/kotlin/net/halfbinary/scavengerhuntapi/model/converter/HunterConverter.kt @@ -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) } \ No newline at end of file diff --git a/src/main/kotlin/net/halfbinary/scavengerhuntapi/model/converter/LoginConverter.kt b/src/main/kotlin/net/halfbinary/scavengerhuntapi/model/converter/LoginConverter.kt new file mode 100644 index 0000000..7f4a071 --- /dev/null +++ b/src/main/kotlin/net/halfbinary/scavengerhuntapi/model/converter/LoginConverter.kt @@ -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) +} \ No newline at end of file diff --git a/src/main/kotlin/net/halfbinary/scavengerhuntapi/model/domain/Login.kt b/src/main/kotlin/net/halfbinary/scavengerhuntapi/model/domain/Login.kt new file mode 100644 index 0000000..68539a7 --- /dev/null +++ b/src/main/kotlin/net/halfbinary/scavengerhuntapi/model/domain/Login.kt @@ -0,0 +1,6 @@ +package net.halfbinary.scavengerhuntapi.model.domain + +data class Login( + val email: String, + val password: String +) diff --git a/src/main/kotlin/net/halfbinary/scavengerhuntapi/model/request/LoginRequest.kt b/src/main/kotlin/net/halfbinary/scavengerhuntapi/model/request/LoginRequest.kt new file mode 100644 index 0000000..4bb018e --- /dev/null +++ b/src/main/kotlin/net/halfbinary/scavengerhuntapi/model/request/LoginRequest.kt @@ -0,0 +1,6 @@ +package net.halfbinary.scavengerhuntapi.model.request + +data class LoginRequest( + val email: String, + val password: String +) diff --git a/src/main/kotlin/net/halfbinary/scavengerhuntapi/model/response/LoginResponse.kt b/src/main/kotlin/net/halfbinary/scavengerhuntapi/model/response/LoginResponse.kt new file mode 100644 index 0000000..c98dc89 --- /dev/null +++ b/src/main/kotlin/net/halfbinary/scavengerhuntapi/model/response/LoginResponse.kt @@ -0,0 +1,6 @@ +package net.halfbinary.scavengerhuntapi.model.response + +data class LoginResponse( + val email: String, + val name: String +) diff --git a/src/main/kotlin/net/halfbinary/scavengerhuntapi/repository/HunterRepository.kt b/src/main/kotlin/net/halfbinary/scavengerhuntapi/repository/HunterRepository.kt index 254cf4b..980dd24 100644 --- a/src/main/kotlin/net/halfbinary/scavengerhuntapi/repository/HunterRepository.kt +++ b/src/main/kotlin/net/halfbinary/scavengerhuntapi/repository/HunterRepository.kt @@ -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 interface HunterRepository : JpaRepository { 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? } \ No newline at end of file diff --git a/src/main/kotlin/net/halfbinary/scavengerhuntapi/service/LoginService.kt b/src/main/kotlin/net/halfbinary/scavengerhuntapi/service/LoginService.kt new file mode 100644 index 0000000..1a73d62 --- /dev/null +++ b/src/main/kotlin/net/halfbinary/scavengerhuntapi/service/LoginService.kt @@ -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() + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/halfbinary/scavengerhuntapi/service/SignupService.kt b/src/main/kotlin/net/halfbinary/scavengerhuntapi/service/SignupService.kt index 068f431..38cf292 100644 --- a/src/main/kotlin/net/halfbinary/scavengerhuntapi/service/SignupService.kt +++ b/src/main/kotlin/net/halfbinary/scavengerhuntapi/service/SignupService.kt @@ -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}") } } \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..47631c0 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + -- 2.49.1