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)
Why always me?