Create a new package com.endlessuphill.regent.web. Inside, create AuthController.kt:

package com.endlessuphill.regent.web
 
import com.endlessuphill.regent.dto.LoginRequest
import com.endlessuphill.regent.dto.RegisterRequest
import com.endlessuphill.regent.dto.AuthResponse
import com.endlessuphill.regent.service.AuthService
import jakarta.validation.Valid // For validating request bodies
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
 
@RestController
@RequestMapping("/api/auth")
class AuthController(private val authService: AuthService) {
    @PostMapping("/register")
    @ResponseStatus(HttpStatus.CREATED) // Return 201 on successful registration
    suspend fun register(@Valid @RequestBody request: RegisterRequest) {
        // @Valid triggers bean validation on the request DTO
        authService.register(request)
        // No need to return user details, just success status
    }
    @PostMapping("/login")
    suspend fun login(@Valid @RequestBody request: LoginRequest): ResponseEntity<AuthResponse> {
        val response = authService.login(request)
        return ResponseEntity.ok(response) // Return 200 OK with token
    }
}

Create a new package com.endlessuphill.regent.dto. Inside, create AuthDtos.kt

package com.endlessuphill.regent.dto
 
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Size
 
data class RegisterRequest(
    @field:NotBlank(message = "Username cannot be blank")
    @field:Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters")
    val username: String,
    @field:NotBlank(message = "Password cannot be blank")
    @field:Size(min = 8, max = 100, message = "Password must be between 8 and 100 characters")
    val password: String
)
 
data class LoginRequest(
    @field:NotBlank(message = "Username cannot be blank")
    val username: String,
    @field:NotBlank(message = "Password cannot be blank")
    val password: String
)
 
data class AuthResponse(
    val token: String
)
 

Create a new package com.endlessuphill.regent.service. Inside, create AuthService.kt:

package com.endlessuphill.regent.service
import com.endlessuphill.regent.data.model.User
import com.endlessuphill.regent.data.repository.UserRepository
import com.endlessuphill.regent.dto.AuthResponse
import com.endlessuphill.regent.dto.LoginRequest
import com.endlessuphill.regent.dto.RegisterRequest
import com.endlessuphill.regent.exception.AuthException // Custom exception (create below)
import com.endlessuphill.regent.exception.UserAlreadyExistsException // Custom exception (create below)
import com.endlessuphill.regent.security.JwtService
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional // Important for DB operations
 
 
@Service
class AuthService(
    private val userRepository: UserRepository,
    private val passwordEncoder: PasswordEncoder,
    private val jwtService: JwtService
) {
 
    @Transactional
    suspend fun register(request: RegisterRequest): User {
        if (userRepository.findByUsername(request.username) != null) {
            throw UserAlreadyExistsException("Username '${request.username}' is already taken.")
        }
        val newUser = User(
            username = request.username,
            hashedPassword = passwordEncoder.encode(request.password)
            // id and createdAt have defaults
        )
        return userRepository.save(newUser)
    }
    // Login doesn't modify data, so @Transactional isn't strictly needed,
    // but it doesn't hurt unless performance critical.
    suspend fun login(request: LoginRequest): AuthResponse {
        val user = userRepository.findByUsername(request.username)
            ?: throw AuthException("Invalid username or password.") // User not found
        if (!passwordEncoder.matches(request.password, user.hashedPassword)) {
            throw AuthException("Invalid username or password.") // Password mismatch
        }
        // User found and password matches, generate token
        val token = jwtService.generateToken(user.id, user.username)
        return AuthResponse(token)
    }
}

Create a new package com.endlessuphill.regent.exception. Inside, create AppExceptions.kt:

package com.endlessuphill.regent.exception
 
import org.springframework.http.HttpStatus
import org.springframework.web.server.ResponseStatusException
 
// For registration conflict
class UserAlreadyExistsException(message: String) : ResponseStatusException(HttpStatus.CONFLICT, message)
 
// For login failures (use generic message for security)
class AuthException(message: String = "Authentication failed") : ResponseStatusException(HttpStatus.UNAUTHORIZED, message)
 
// General bad request
class BadRequestException(message: String) : ResponseStatusException(HttpStatus.BAD_REQUEST, message)