Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ trim_trailing_whitespace = true
insert_final_newline = true

[*.kt]
max_line_length = 120
max_line_length = 144
ij_kotlin_allow_trailing_comma = true
Original file line number Diff line number Diff line change
@@ -1,30 +1,33 @@
package com.lightswitch.application.service

import com.lightswitch.presentation.exception.BusinessException
import com.lightswitch.infrastructure.database.entity.RefreshToken
import com.lightswitch.infrastructure.database.entity.User
import com.lightswitch.infrastructure.database.repository.RefreshTokenRepository
import com.lightswitch.infrastructure.database.repository.UserRepository
import com.lightswitch.infrastructure.security.JwtToken
import com.lightswitch.infrastructure.security.JwtTokenProvider
import com.lightswitch.presentation.exception.BusinessException
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
import java.util.*
import java.util.Date

@Service
class AuthService(
private val jwtTokenProvider: JwtTokenProvider,
val userRepository: UserRepository,
val refreshTokenRepository: RefreshTokenRepository,
val passwordEncoder: PasswordEncoder
val passwordEncoder: PasswordEncoder,
) {

@Transactional
fun login(username: String, password: String): JwtToken {
val user = userRepository.findByUsername(username)
?: throw BusinessException("User with username $username not found")
fun login(
username: String,
password: String,
): JwtToken {
val user =
userRepository.findByUsername(username)
?: throw BusinessException("User with username $username not found")

if (!passwordEncoder.matches(password, user.passwordHash)) {
throw BusinessException("Password is incorrect")
Expand All @@ -39,37 +42,47 @@ class AuthService(
}

@Transactional
fun signup(username: String, password: String): User {
fun signup(
username: String,
password: String,
): User {
if (userRepository.existsByUsername(username)) {
throw BusinessException("Username already exists")
}
val passwordHash = passwordEncoder.encode(password)

val newUser = User(
username = username,
passwordHash = passwordHash
)
val newUser =
User(
username = username,
passwordHash = passwordHash,
)
return userRepository.save(newUser)
}

@Transactional
fun reissue(jwtToken: String, userId: Long): JwtToken? {
val refreshToken: RefreshToken = refreshTokenRepository.findById(userId)
.orElseThrow { BusinessException("Log-out user") }
fun reissue(
jwtToken: String,
userId: Long,
): JwtToken? {
val refreshToken: RefreshToken =
refreshTokenRepository.findById(userId)
.orElseThrow { BusinessException("Log-out user") }

if (refreshToken.value != jwtToken) {
throw BusinessException("Refresh Token is Not Valid")
}

val user = userRepository.findById(userId)
.orElseThrow { BusinessException("User not found") }
val user =
userRepository.findById(userId)
.orElseThrow { BusinessException("User not found") }

return when {
jwtTokenProvider.isRefreshTokenRenewalRequired(refreshToken.value) -> {
jwtTokenProvider.generateJwtToken(userId, user).also {
refreshToken.value = it.refreshToken.toString()
}
}

else -> {
jwtTokenProvider.generateJwtAccessToken(userId, user, Date(), refreshToken.value)
}
Expand All @@ -80,4 +93,4 @@ class AuthService(
fun logout(userId: Long) {
refreshTokenRepository.deleteById(userId)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,57 +10,72 @@ import jakarta.validation.Valid
import jakarta.validation.constraints.NotEmpty
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.bind.annotation.*
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/api/v1/users")
class UserController(private val authService: AuthService) {
@Operation(
summary = "Login the user",
description = "Authenticates a user with their username and password, and returns a JWT token."
description = "Authenticates a user with their username and password, and returns a JWT token.",
)
@PostMapping("/login")
fun userLogin(@RequestBody @Valid body: UserAccount): PayloadResponse<JwtToken> {
fun userLogin(
@RequestBody @Valid body: UserAccount,
): PayloadResponse<JwtToken> {
val token = authService.login(body.username, body.password)
return PayloadResponse(
"Success",
message = "User Login Success",
data = token
data = token,
)
}

@Operation(
summary = "Initialize user account",
description = "Registers a new user with the provided username and password. Returns a confirmation message."
description = "Registers a new user with the provided username and password. Returns a confirmation message.",
)
@PostMapping("/initialize")
fun userInitialize(@RequestBody @Valid body: UserAccount): PayloadResponse<String> {
fun userInitialize(
@RequestBody @Valid body: UserAccount,
): PayloadResponse<String> {
val user = authService.signup(body.username, body.password)
return PayloadResponse(
"Success",
message = "Signup New User Success",
data = user.username
data = user.username,
)
}

@Operation(
summary = "Refresh authentication token",
description = "Reissues a new JWT token using the provided refresh token and current user's identity."
description = "Reissues a new JWT token using the provided refresh token and current user's identity.",
)
@PutMapping("/auth/refresh")
fun refreshUserToken(@RequestHeader("Authorization") @NotEmpty @Parameter(description = "The refresh token prefixed with 'Bearer '.") refreshToken: String): PayloadResponse<JwtToken> {
fun refreshUserToken(
@RequestHeader(
"Authorization",
) @NotEmpty @Parameter(description = "The refresh token prefixed with 'Bearer '.") refreshToken: String,
): PayloadResponse<JwtToken> {
val authentication: Authentication = SecurityContextHolder.getContext().authentication
val token = authService.reissue(refreshToken.removePrefix("Bearer "), authentication.name.toLong())
return PayloadResponse("Success", "Refresh Token Success", token)
}

@Operation(
summary = "Logout the user",
description = "Logs out the user by invalidating their access token. Requires the user to provide their token."
description = "Logs out the user by invalidating their access token. Requires the user to provide their token.",
)
@PostMapping("/logout")
fun userLogout(
@RequestHeader("Authorization") @NotEmpty @Parameter(description = "The access token prefixed with 'Bearer '.") accessToken: String,
@RequestHeader(
"Authorization",
) @NotEmpty @Parameter(description = "The access token prefixed with 'Bearer '.") accessToken: String,
@RequestBody @Valid body: UserAccount,
): PayloadResponse<String> {
val authentication: Authentication = SecurityContextHolder.getContext().authentication
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ data class UserAccount(
val username: String,
@Schema(description = "The password of the user.")
@field:NotBlank(message = "Password is required.")
val password: String
)
val password: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import jakarta.persistence.Entity
import jakarta.persistence.Id

@Entity
class RefreshToken (
class RefreshToken(
@Id
@Column(nullable = false)
val userId: Long,
@Column(nullable = false)
var value: String
): BaseEntity()
var value: String,
) : BaseEntity()
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ package com.lightswitch.infrastructure.database.model
enum class Type {
NUMBER,
BOOLEAN,
STRING;
STRING,
;

companion object {
fun from(type: String): Type {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ package com.lightswitch.infrastructure.database.repository
import com.lightswitch.infrastructure.database.entity.RefreshToken
import org.springframework.data.jpa.repository.JpaRepository

interface RefreshTokenRepository: JpaRepository<RefreshToken, Long>
interface RefreshTokenRepository : JpaRepository<RefreshToken, Long>
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ package com.lightswitch.infrastructure.database.repository
import com.lightswitch.infrastructure.database.entity.User
import org.springframework.data.jpa.repository.JpaRepository

interface UserRepository : JpaRepository<User, Long>{
interface UserRepository : JpaRepository<User, Long> {
fun existsByUsername(username: String): Boolean

fun findByUsername(username: String): User?
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,22 @@ package com.lightswitch.infrastructure.security

import io.swagger.v3.oas.annotations.media.Schema

@Schema(description = "Represents the JWT token response containing both access and refresh tokens, as well as the expiration date of the access token.")
@Schema(
description =
"Represents the JWT token response containing both access and refresh tokens, " +
"as well as the expiration date of the access token.",
)
data class JwtToken(
@Schema(description = "The access token used to authenticate the user in subsequent requests. This token is valid for 30 minutes.")
@Schema(
description = "The access token used to authenticate the user in subsequent requests. This token is valid for 30 minutes.",
)
val accessToken: String? = null,
@Schema(description = "The refresh token used to obtain a new access token once it expires. This token is valid for 7 days.")
@Schema(
description = "The refresh token used to obtain a new access token once it expires. This token is valid for 7 days.",
)
val refreshToken: String? = null,
@Schema(description = "The expiration date (timestamp in milliseconds) of the access token. The token becomes invalid after this time.")
val accessTokenExpiredDate: Long? = null
@Schema(
description = "The expiration date (timestamp in milliseconds) of the access token. The token becomes invalid after this time.",
)
val accessTokenExpiredDate: Long? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import java.io.IOException
class JwtTokenFilter(
private val jwtTokenProvider: JwtTokenProvider,
) : OncePerRequestFilter() {

companion object {
const val HEADER_STRING = "Authorization"
const val TOKEN_PREFIX = "Bearer "
Expand All @@ -23,7 +22,7 @@ class JwtTokenFilter(
public override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
filterChain: FilterChain,
) {
getTokenFromRequest(request)
?.takeIf { jwtTokenProvider.validateToken(it) }
Expand All @@ -38,4 +37,4 @@ class JwtTokenFilter(
?.takeIf { it.startsWith(TOKEN_PREFIX) }
?.removePrefix(TOKEN_PREFIX)
}
}
}
Loading
Loading