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
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package until.the.eternity.auctionhistory.interfaces.rest.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import until.the.eternity.auctionhistory.application.scheduler.AuctionHistoryScheduler;
import until.the.eternity.auctionhistory.application.service.AuctionHistoryService;
Expand Down Expand Up @@ -45,9 +47,11 @@ public ResponseEntity<AuctionHistoryDetailResponse<ItemOptionResponse>> findById
// TODO: 응답 형식도 몇건씩 저장됐는지 결과를 보내줘야하나 고민
// TODO: 실행 시 OPEN API Key를 따로 받을지 고민
@PostMapping("/batch")
@PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')")
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

hasAnyRole('ADMIN','SUPER_ADMIN') relies on roles being mapped as separate authorities (ROLE_ADMIN / ROLE_SUPER_ADMIN). GatewayAuthFilter currently reads X-Auth-Roles as a single role string and doesn’t split multi-role headers (even though its header example suggests multiple roles), which can cause admins to be denied here. Please update role parsing (comma-split) or enforce a single-role header contract.

Suggested change
@PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')")
@PreAuthorize("hasAnyAuthority('ADMIN', 'SUPER_ADMIN')")

Copilot uses AI. Check for mistakes.
@Operation(
summary = "경매장 거래 내역 배치 실행",
description = "Nexon Open API 경매장 거래 내역 모든 카테고리 데이터 INSERT 배치 실행")
description = "Nexon Open API 경매장 거래 내역 모든 카테고리 데이터 INSERT 배치 실행. **[ADMIN, SUPER_ADMIN 전용]**")
@ApiResponse(responseCode = "403", description = "권한 없음 (ADMIN, SUPER_ADMIN 전용)")
public ResponseEntity<Void> triggerMinPriceBatch() {
scheduler.fetchAndSaveAuctionHistoryAll();
return ResponseEntity.ok().build();
Expand Down
28 changes: 28 additions & 0 deletions src/main/java/until/the/eternity/common/enums/UserRole.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package until.the.eternity.common.enums;

import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum UserRole {
USER("user", "일반 사용자"),
ADMIN("admin", "관리자"),
SUPER_ADMIN("super_admin", "최고 관리자");

private final String code;
private final String description;

private static final Map<String, UserRole> CODE_MAP =
Arrays.stream(values())
.collect(Collectors.toMap(UserRole::getCode, Function.identity()));

public static Optional<UserRole> fromCode(String code) {
return Optional.ofNullable(CODE_MAP.get(code));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -14,17 +17,14 @@
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import until.the.eternity.common.entity.CustomWebAuthenticationDetails;
import until.the.eternity.common.enums.UserRole;
import until.the.eternity.common.util.IpAddressUtil;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
* Gateway에서 전달한 인증 헤더(X-Auth-*)를 기반으로 Spring Security의 Authentication을 생성하는 필터
*
* <p>Gateway에서 전달하는 헤더: - X-Auth-User-Id: 사용자 ID (Long) - X-Auth-Username: 사용자 이메일/username
* (String) - X-Auth-Roles: 사용자 역할 (예: ROLE_USER, ROLE_ADMIN)
* (String) - X-Auth-Roles: 사용자 역할 (예: user, admin, super_admin)
*/
@Slf4j
@Component
Expand All @@ -40,13 +40,12 @@ protected void doFilterInternal(
@NonNull FilterChain filterChain)
throws ServletException, IOException {

// Gateway에서 전달한 인증 헤더 읽기
String userIdHeader = request.getHeader("X-Auth-User-Id");
String usernameHeader = request.getHeader("X-Auth-Username");
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The usernameHeader variable is read from the request but never used after the refactoring. It's only referenced in the debug log on line 62, but it's not passed to the getAuthentication method anymore. Consider removing this unused variable to improve code clarity, or if it's needed for logging purposes, document that explicitly.

Copilot uses AI. Check for mistakes.
String rolesHeader = request.getHeader("X-Auth-Roles");

UsernamePasswordAuthenticationToken authentication =
getAuthentication(userIdHeader, usernameHeader, rolesHeader);
getAuthentication(userIdHeader, rolesHeader);

String clientIp = ipAddressUtil.getClientIp(request);

Expand All @@ -57,23 +56,18 @@ protected void doFilterInternal(

SecurityContextHolder.getContext().setAuthentication(authentication);

log.debug("Authentication set for user: {} with roles: {}", usernameHeader, rolesHeader);
log.debug(
"Authentication set - Principal: {}, Username: {}, Authorities: {}",
authentication.getPrincipal(),
usernameHeader,
authentication.getAuthorities());

filterChain.doFilter(request, response);
}

/**
* Gateway에서 전달한 헤더 정보를 기반으로 Authentication 생성
*
* @param userIdHeader 사용자 ID (Gateway에서 검증됨)
* @param usernameHeader 사용자 이메일/username (Gateway에서 검증됨)
* @param rolesHeader 사용자 역할 (Gateway에서 검증됨)
* @return UsernamePasswordAuthenticationToken
*/
private UsernamePasswordAuthenticationToken getAuthentication(
String userIdHeader, String usernameHeader, String rolesHeader) {
String userIdHeader, String rolesHeader) {

// User ID 파싱
Long userId = null;
try {
if (userIdHeader != null) {
Expand All @@ -83,28 +77,23 @@ private UsernamePasswordAuthenticationToken getAuthentication(
log.warn("Invalid user ID header: {}", userIdHeader);
}

// Roles가 없으면 익명 사용자로 처리
if (rolesHeader == null || rolesHeader.isEmpty()) {
log.debug("No roles found, creating anonymous authentication");
return new UsernamePasswordAuthenticationToken(userId, null);
}

// Roles 파싱 (ROLE_USER, ROLE_ADMIN 등)
List<GrantedAuthority> authorities = new ArrayList<>();
String roleCode = rolesHeader.trim().toLowerCase();
UserRole userRole = UserRole.fromCode(roleCode).orElse(null);

// Gateway에서 넘어온 role이 이미 "ROLE_" prefix를 가지고 있으므로 그대로 사용
// 단, "ROLE_" prefix가 없으면 추가
String role = rolesHeader.trim();
if (!role.startsWith("ROLE_")) {
role = "ROLE_" + role;
if (userRole == null) {
log.warn("Unknown role code: '{}', creating anonymous authentication", roleCode);
return new UsernamePasswordAuthenticationToken(userId, null);
}
authorities.add(new SimpleGrantedAuthority(role));

log.debug(
"Created authentication for userId: {}, username: {}, role: {}",
userId,
usernameHeader,
role);
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_" + userRole.name()));

log.debug("Created authentication for userId: {}, role: {}", userId, userRole.name());

return new UsernamePasswordAuthenticationToken(userId, null, authorities);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Size;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import until.the.eternity.common.response.PageResponseDto;
Expand Down Expand Up @@ -49,7 +51,9 @@ public ResponseEntity<PageResponseDto<HornBugleHistoryResponse>> search(
}

@PostMapping("/batch")
@Operation(summary = "뿔피리 히스토리 배치 실행", description = "모든 서버의 거대한 외침의 뿔피리 내역을 수집하여 저장합니다.")
@PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')")
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

This new @PreAuthorize check will fail if X-Auth-Roles is delivered as a comma-separated list (e.g., ROLE_USER, ROLE_ADMIN) because GatewayAuthFilter currently turns the entire header into a single authority value. Consider updating role parsing in the filter so hasAnyRole('ADMIN','SUPER_ADMIN') behaves correctly for multi-role users.

Suggested change
@PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')")
@PreAuthorize(
"authentication != null && " +
"authentication.authorities.?[" +
" T(org.springframework.util.StringUtils)" +
" .commaDelimitedListToSet(authority)" +
" .contains('ROLE_ADMIN') " +
" or " +
" T(org.springframework.util.StringUtils)" +
" .commaDelimitedListToSet(authority)" +
" .contains('ROLE_SUPER_ADMIN')" +
"].size() > 0")

Copilot uses AI. Check for mistakes.
@Operation(summary = "뿔피리 히스토리 배치 실행", description = "모든 서버의 거대한 외침의 뿔피리 내역을 수집하여 저장합니다. **[ADMIN, SUPER_ADMIN 전용]**")
@ApiResponse(responseCode = "403", description = "권한 없음 (ADMIN, SUPER_ADMIN 전용)")
public ResponseEntity<Void> triggerBatch() {
log.info("[HornBugle] Batch API triggered");
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import until.the.eternity.common.enums.SortDirection;
import until.the.eternity.common.response.ApiResponse;
Expand Down Expand Up @@ -78,7 +79,10 @@ public ResponseEntity<ApiResponse<List<ItemInfoSummaryResponse>>> getItemInfosSu
summary = "경매 내역에서 아이템 정보 동기화",
description =
"AuctionHistory 테이블에서 아이템 정보를 조회하여 ItemInfo 테이블에 동기화합니다. "
+ "이미 존재하는 아이템은 제외하고 새로운 아이템만 추가합니다.")
+ "이미 존재하는 아이템은 제외하고 새로운 아이템만 추가합니다. "
+ "**[ADMIN, SUPER_ADMIN 전용]**")
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "권한 없음 (ADMIN, SUPER_ADMIN 전용)")
@PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')")
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

This endpoint is now role-protected via hasAnyRole('ADMIN','SUPER_ADMIN'). Please ensure the gateway role header is parsed into separate GrantedAuthority entries; GatewayAuthFilter currently treats X-Auth-Roles as a single string and doesn’t split comma-separated roles (despite its own example), which can prevent even valid admins from calling this API.

Suggested change
@PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')")
@PreAuthorize("hasAnyAuthority('ADMIN', 'SUPER_ADMIN')")

Copilot uses AI. Check for mistakes.
@PostMapping("/sync")
public ResponseEntity<ApiResponse<ItemInfoSyncResponse>> syncItemInfoFromAuctionHistory() {
ItemInfoSyncResponse response = itemInfoService.syncItemInfoFromAuctionHistory();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package until.the.eternity.itemoptioninfo.interfaces.rest.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import until.the.eternity.itemoptioninfo.application.service.ItemOptionInfoService;
import until.the.eternity.itemoptioninfo.domain.entity.ItemOptionInfo;
Expand Down Expand Up @@ -35,18 +37,22 @@ public List<ItemOptionInfoResponse> findAll() {

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
@Operation(summary = "아이템 옵션 정보 생성", description = "새로운 아이템 옵션 정보를 생성합니다.")
@PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')")
@Operation(summary = "아이템 옵션 정보 생성", description = "새로운 아이템 옵션 정보를 생성합니다. **[ADMIN, SUPER_ADMIN 전용]**")
@ApiResponse(responseCode = "403", description = "권한 없음 (ADMIN, SUPER_ADMIN 전용)")
Comment on lines 39 to +42
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

The new hasAnyRole('ADMIN','SUPER_ADMIN') authorization will only work if the gateway-to-service role mapping populates ROLE_ADMIN/ROLE_SUPER_ADMIN as separate authorities. GatewayAuthFilter currently reads X-Auth-Roles as a single string and doesn’t split multiple roles (even though its docs show values like ROLE_USER, ROLE_ADMIN), which can cause all admin requests to be denied. Please update the role parsing (e.g., split on commas) or tighten the header contract.

Copilot uses AI. Check for mistakes.
public ItemOptionInfoResponse create(@Valid @RequestBody ItemOptionInfoRequest request) {
ItemOptionInfo itemOptionInfo = itemOptionInfoMapper.toEntity(request);
ItemOptionInfo saved = itemOptionInfoService.create(itemOptionInfo);
return itemOptionInfoMapper.toItemOptionInfoResponse(saved);
}

@PutMapping
@PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')")
@Operation(
Comment on lines 49 to 51
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

Same concern as other @PreAuthorize usages: if X-Auth-Roles contains multiple roles, GatewayAuthFilter currently turns it into a single GrantedAuthority string, so hasAnyRole('ADMIN','SUPER_ADMIN') may never match and this update endpoint will be blocked for valid admins. Consider fixing role parsing in the filter or enforcing a single-role header.

Copilot uses AI. Check for mistakes.
summary = "아이템 옵션 정보 수정",
description =
"아이템 옵션 정보를 수정합니다. 복합 키(optionType, optionSubType, optionValue, optionValue2)로 식별하며, optionDesc만 수정 가능합니다.")
"아이템 옵션 정보를 수정합니다. 복합 키(optionType, optionSubType, optionValue, optionValue2)로 식별하며, optionDesc만 수정 가능합니다. **[ADMIN, SUPER_ADMIN 전용]**")
@ApiResponse(responseCode = "403", description = "권한 없음 (ADMIN, SUPER_ADMIN 전용)")
public ItemOptionInfoResponse update(@Valid @RequestBody ItemOptionInfoRequest request) {
ItemOptionInfoId id = itemOptionInfoMapper.toId(request);
ItemOptionInfo updated = itemOptionInfoService.update(id, request.getOptionDesc());
Expand All @@ -55,10 +61,12 @@ public ItemOptionInfoResponse update(@Valid @RequestBody ItemOptionInfoRequest r

@DeleteMapping
@ResponseStatus(HttpStatus.NO_CONTENT)
@PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')")
@Operation(
summary = "아이템 옵션 정보 삭제",
Comment on lines 63 to 66
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

Same concern as other @PreAuthorize usages: this delete endpoint can become unreachable for admins if the gateway sends multiple roles in X-Auth-Roles because GatewayAuthFilter doesn’t currently split them into separate authorities. Updating the filter to parse multiple roles (comma-separated) would make hasAnyRole('ADMIN','SUPER_ADMIN') reliable.

Copilot uses AI. Check for mistakes.
description =
"아이템 옵션 정보를 삭제합니다. 복합 키(optionType, optionSubType, optionValue, optionValue2)로 식별합니다.")
"아이템 옵션 정보를 삭제합니다. 복합 키(optionType, optionSubType, optionValue, optionValue2)로 식별합니다. **[ADMIN, SUPER_ADMIN 전용]**")
@ApiResponse(responseCode = "403", description = "권한 없음 (ADMIN, SUPER_ADMIN 전용)")
public void delete(@Valid @RequestBody ItemOptionInfoRequest request) {
ItemOptionInfoId id = itemOptionInfoMapper.toId(request);
itemOptionInfoService.delete(id);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package until.the.eternity.metalwareinfo.interfaces.rest.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import until.the.eternity.common.response.PageResponseDto;
import until.the.eternity.metalwareinfo.application.service.MetalwareAttributeInfoService;
Expand All @@ -20,7 +22,9 @@ public class MetalwareAttributeInfoController {

private final MetalwareAttributeInfoService metalwareAttributeInfoService;

@Operation(summary = "세공 능력치 정보 동기화", description = "경매 기록에서 세공 능력치 정보를 추출하여 동기화합니다.")
@Operation(summary = "세공 능력치 정보 동기화", description = "경매 기록에서 세공 능력치 정보를 추출하여 동기화합니다. **[ADMIN, SUPER_ADMIN 전용]**")
@ApiResponse(responseCode = "403", description = "권한 없음 (ADMIN, SUPER_ADMIN 전용)")
@PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')")
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

This role guard depends on X-Auth-Roles being mapped to Spring GrantedAuthority values like ROLE_ADMIN/ROLE_SUPER_ADMIN. Currently GatewayAuthFilter only creates a single authority from the header value and does not split multiple roles (despite its own header example showing multiple roles). If the gateway sends comma-separated roles, admin users will still get 403 here; consider updating the filter to parse multiple roles or enforcing a single-role header format.

Suggested change
@PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')")
@PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN') or " +
"(authentication != null and authentication.authorities.?[authority != null and (authority.contains('ROLE_ADMIN') or authority.contains('ROLE_SUPER_ADMIN'))].size() > 0)")

Copilot uses AI. Check for mistakes.
@PostMapping("/sync")
public ResponseEntity<Integer> sync() {
int syncedCount = metalwareAttributeInfoService.sync();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package until.the.eternity.metalwareinfo.interfaces.rest.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
Expand Down Expand Up @@ -33,7 +35,10 @@ public List<MetalwareInfoResponse> getAllMetalwareInfos() {
summary = "세공 정보 동기화",
description =
"metalware_attribute_info를 기반으로 metalware_info를 업서트합니다. "
+ "레벨 1 attribute 동기화와 금속별 최대 레벨(limit_break_level) 동기화를 한 번에 수행합니다.")
+ "레벨 1 attribute 동기화와 금속별 최대 레벨(limit_break_level) 동기화를 한 번에 수행합니다. "
+ "**[ADMIN, SUPER_ADMIN 전용]**")
@ApiResponse(responseCode = "403", description = "권한 없음 (ADMIN, SUPER_ADMIN 전용)")
@PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')")
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

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

@PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')") assumes the authenticated user has either ROLE_ADMIN or ROLE_SUPER_ADMIN. However, GatewayAuthFilter currently treats X-Auth-Roles as a single role string (it doesn’t split comma-separated roles even though its Javadoc suggests values like ROLE_USER, ROLE_ADMIN). If the gateway sends multiple roles, this check will never match and the endpoint will be inaccessible; update the auth filter to parse and register all roles (e.g., split on commas) or align the header contract to a single role.

Suggested change
@PreAuthorize("hasAnyRole('ADMIN', 'SUPER_ADMIN')")
@PreAuthorize("hasAuthority('ROLE_ADMIN, ROLE_SUPER_ADMIN')")

Copilot uses AI. Check for mistakes.
@PostMapping("/sync")
public ResponseEntity<MetalwareInfoSyncResponse> syncMetalwareInfo() {
return ResponseEntity.ok(metalwareInfoService.syncFromAttributeInfo());
Expand Down