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: 2 additions & 0 deletions src/main/kotlin/com/cw/vlainter/VlainterApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import org.springframework.boot.runApplication
import org.springframework.data.jpa.repository.config.EnableJpaAuditing
import org.springframework.scheduling.annotation.EnableAsync
import org.springframework.scheduling.annotation.EnableScheduling
import java.util.TimeZone

@SpringBootApplication
@ConfigurationPropertiesScan
Expand All @@ -15,5 +16,6 @@ import org.springframework.scheduling.annotation.EnableScheduling
class VlainterApplication

fun main(args: Array<String>) {
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

JVM의 기본 시간대를 main 함수에서 직접 설정하는 것보다 Spring의 생명주기에 맞춰 관리하는 것이 더 좋습니다. 설정을 중앙에서 관리하고 테스트 용이성을 높이기 위해, 이 로직을 @SpringBootApplication 클래스 내부에 @PostConstruct 어노테이션을 사용한 초기화 메서드로 옮기는 것을 고려해 보세요. 이렇게 하면 애플리케이션의 진입점(entry point)을 더 깔끔하게 유지할 수 있습니다.

runApplication<VlainterApplication>(*args)
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,7 @@ class AuthController(
logger.info("Auth signup success userId={} email={} ip={}", createdUser.id, createdUser.email, clientIp)
return ResponseEntity.ok(
mapOf(
"message" to "회원가입이 완료되었습니다.",
"userId" to createdUser.id,
"email" to createdUser.email,
"name" to createdUser.name
"message" to "회원가입이 완료되었습니다."
)
)
} catch (ex: ResponseStatusException) {
Expand All @@ -93,7 +90,6 @@ class AuthController(
): ResponseEntity<Map<String, Any>> {
return ResponseEntity.ok(
mapOf(
"userId" to principal.userId,
"email" to principal.email
)
)
Expand Down Expand Up @@ -132,7 +128,6 @@ class AuthController(

return ResponseEntity.ok(
LoginResponse(
userId = result.userId,
email = result.email,
name = result.name,
role = result.role,
Expand Down Expand Up @@ -179,7 +174,6 @@ class AuthController(

return ResponseEntity.ok(
LoginResponse(
userId = result.userId,
email = result.email,
name = result.name,
role = result.role,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import com.cw.vlainter.domain.user.entity.UserRole
* 본문에는 화면 전환/표시 용도의 최소 사용자 정보만 포함한다.
*/
data class LoginResponse(
val userId: Long,
val email: String,
val name: String,
val role: UserRole,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,7 @@ class AuthService(
val sessionId = jwtTokenProvider.extractSessionIdFromRefreshToken(refreshToken)
val validSession = loginSessionStore.validateRefreshToken(sessionId, userId, refreshToken)
if (!validSession) {
loginSessionStore.delete(sessionId)
throw unauthorizedException()
throw refreshUnauthorizedException()
Comment on lines 177 to +178

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Revoke session when refresh validation fails

When validateRefreshToken returns false, this branch now just returns 401 and leaves the Redis session alive. In the replay/hijack scenario (an old refresh token is reused after rotation, including a stolen token), the session remains valid for whoever holds the latest refresh token, so the anomaly no longer forces containment. Previously deleting sid here revoked the whole session family; removing that revocation weakens token-theft recovery behavior.

Useful? React with 👍 / 👎.

}
Comment on lines 177 to 179

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

리프레시 토큰 검증 실패 시 세션을 삭제하지 않도록 변경하면 여러 탭/기기에서의 사용자 경험이 향상될 수 있습니다. 하지만 이는 보안상 중요한 '리프레시 토큰 재사용 탐지' 기능을 약화시킬 수 있습니다. 토큰 회전(rotation) 전략에서 이전에 사용된 토큰이 다시 등장하는 것은 토큰 탈취의 강력한 신호로 간주됩니다. 이 경우, 보안 강화를 위해 해당 세션을 즉시 만료시키는 것이 좋습니다. 기존의 loginSessionStore.delete(sessionId) 로직이 이러한 방어적 역할을 수행했습니다. 현재 변경은 사용자 편의성을 높이지만, 잠재적인 보안 위협을 탐지하고 대응할 기회를 놓칠 수 있습니다. 사용자 경험과 보안 사이의 균형을 고려하여 이 변경 사항을 재검토해 보시길 권장합니다.


val user = userRepository.findById(userId).orElseThrow { unauthorizedException() }
Expand Down Expand Up @@ -247,6 +246,10 @@ class AuthService(
return ResponseStatusException(HttpStatus.UNAUTHORIZED, "이메일 또는 비밀번호가 올바르지 않습니다.")
}

private fun refreshUnauthorizedException(): ResponseStatusException {
return ResponseStatusException(HttpStatus.UNAUTHORIZED, "인증이 만료되었습니다. 다시 로그인해 주세요.")
}

private fun resolveSocialName(nameHint: String?, email: String): String {
val trimmedName = nameHint?.trim().orEmpty()
if (trimmedName.isNotBlank()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.cw.vlainter.domain.interview.controller

import com.cw.vlainter.domain.interview.dto.AdminQuestionSetSummaryResponse
import com.cw.vlainter.domain.interview.dto.CategoryResponse
import com.cw.vlainter.domain.interview.dto.AdminCategoryResponse
import com.cw.vlainter.domain.interview.dto.CreateCategoryRequest
import com.cw.vlainter.domain.interview.dto.MergeCategoryRequest
import com.cw.vlainter.domain.interview.dto.MoveCategoryRequest
Expand Down Expand Up @@ -65,24 +65,24 @@ class InterviewAdminController(
}

@GetMapping("/categories")
fun getCategories(): ResponseEntity<List<CategoryResponse>> {
return ResponseEntity.ok(categoryAdminService.getActiveCategoryTree())
fun getCategories(): ResponseEntity<List<AdminCategoryResponse>> {
return ResponseEntity.ok(categoryAdminService.getActiveCategoryTreeForAdmin())
}

@PostMapping("/categories")
fun createCategory(
@AuthenticationPrincipal principal: AuthPrincipal,
@Valid @RequestBody request: CreateCategoryRequest
): ResponseEntity<CategoryResponse> {
return ResponseEntity.ok(categoryAdminService.createCategory(principal, request))
): ResponseEntity<AdminCategoryResponse> {
return ResponseEntity.ok(categoryAdminService.createCategoryForAdmin(principal, request))
}

@PatchMapping("/categories/{categoryId}")
fun updateCategory(
@AuthenticationPrincipal principal: AuthPrincipal,
@PathVariable categoryId: Long,
@RequestBody request: UpdateCategoryRequest
): ResponseEntity<CategoryResponse> {
): ResponseEntity<AdminCategoryResponse> {
return ResponseEntity.ok(categoryAdminService.updateCategory(principal, categoryId, request))
}

Expand All @@ -91,7 +91,7 @@ class InterviewAdminController(
@AuthenticationPrincipal principal: AuthPrincipal,
@PathVariable categoryId: Long,
@Valid @RequestBody request: MoveCategoryRequest
): ResponseEntity<CategoryResponse> {
): ResponseEntity<AdminCategoryResponse> {
return ResponseEntity.ok(categoryAdminService.moveCategory(principal, categoryId, request))
}

Expand All @@ -100,7 +100,7 @@ class InterviewAdminController(
@AuthenticationPrincipal principal: AuthPrincipal,
@PathVariable categoryId: Long,
@Valid @RequestBody request: MergeCategoryRequest
): ResponseEntity<CategoryResponse> {
): ResponseEntity<AdminCategoryResponse> {
return ResponseEntity.ok(categoryAdminService.mergeCategory(principal, categoryId, request))
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.cw.vlainter.domain.interview.controller

import com.cw.vlainter.domain.interview.dto.CategoryResponse
import com.cw.vlainter.domain.interview.dto.CreateCategoryRequest
import com.cw.vlainter.domain.interview.dto.PublicCategoryResponse
import com.cw.vlainter.domain.interview.service.CategoryAdminService
import com.cw.vlainter.global.security.AuthPrincipal
import jakarta.validation.Valid
Expand All @@ -20,16 +20,16 @@ class InterviewCatalogController(
private val categoryAdminService: CategoryAdminService
) {
@GetMapping("/categories")
fun getCategoryTree(): ResponseEntity<List<CategoryResponse>> {
return ResponseEntity.ok(categoryAdminService.getActiveCategoryTree())
fun getCategoryTree(): ResponseEntity<List<PublicCategoryResponse>> {
return ResponseEntity.ok(categoryAdminService.getActiveCategoryTreeForUser())
}

@PostMapping("/categories")
fun createCategory(
@AuthenticationPrincipal principal: AuthPrincipal,
@Valid @RequestBody request: CreateCategoryRequest
): ResponseEntity<CategoryResponse> {
): ResponseEntity<PublicCategoryResponse> {
return ResponseEntity.status(HttpStatus.CREATED)
.body(categoryAdminService.createCategory(principal, request))
.body(categoryAdminService.createCategoryForUser(principal, request))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.cw.vlainter.domain.interview.dto
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Size
import com.cw.vlainter.domain.user.entity.UserStatus

data class CreateCategoryRequest(
val parentId: Long? = null,
@field:Size(max = 80, message = "카테고리 코드는 80자 이하여야 합니다.")
Expand Down Expand Up @@ -32,7 +31,21 @@ data class MergeCategoryRequest(
val targetCategoryId: Long
)

data class CategoryResponse(
data class PublicCategoryResponse(
val categoryId: Long,
val parentId: Long?,
val code: String,
val name: String,
val description: String?,
val depth: Int,
val depthLabel: String,
val path: String,
val sortOrder: Int,
val isActive: Boolean,
val isLeaf: Boolean
)

data class AdminCategoryResponse(
val categoryId: Long,
val parentId: Long?,
val code: String,
Expand Down
Loading
Loading