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
Expand Up @@ -250,17 +250,18 @@ public Pageable<FilteringIssue<IssueWithManagers>> getIssuesByTeamId(

// 담당자가 없는 이슈의 개수 조회
Long unassignedCount = issueRepository.findUnassignedIssuesCountByTeamId(teamId);
List<String> sortedManagersList = map.keySet().stream().sorted().collect(Collectors.toList());
map.put("담당자 없음", Math.toIntExact(unassignedCount));
managers.add("담당자 없음");
sortedManagersList.addFirst("담당자 없음");

// firstCursor가 담당자 리스트에 포함되어있는지 확인
if (!managers.contains(firstCursor)) {
if (!sortedManagersList.contains(firstCursor)) {
// 없으면 담당자 없음부터
firstCursor = "담당자 없음";
}

// 담당자 별 데이터 분류
for (String filter : map.keySet().stream().sorted().toList()) {
for (String filter : sortedManagersList) {
List<SimpleIssue> result = new ArrayList<>();

// firstCursor가 일치할 때, 조회 시작
Expand Down Expand Up @@ -320,16 +321,18 @@ public Pageable<FilteringIssue<IssueWithManagers>> getIssuesByTeamId(

// 목표가 없는 이슈의 개수 조회
Long noGoalCount = issueRepository.findNoGoalIssuesCountByTeamId(teamId);
List<String> sortedGoalsList = map.keySet().stream().sorted().collect(Collectors.toList());
map.put("목표 없음", Math.toIntExact(noGoalCount));
sortedGoalsList.addFirst("목표 없음");

// firstCursor가 목표 리스트에 포함되어있는지 확인
if (!goals.contains(firstCursor)) {
if (!sortedGoalsList.contains(firstCursor)) {
// 없으면 목표 없음부터
firstCursor = "목표 없음";
}

// 목표 별 데이터 분류
for (String filter : map.keySet().stream().sorted().toList()) {
for (String filter : sortedGoalsList) {
List<SimpleIssue> result = new ArrayList<>();

// firstCursor가 일치할 때, 조회 시작
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,15 @@ public ApiResponse<List<WorkspaceResponseDTO.WorkspaceMemberWithTeamsDto>> getWo
List<WorkspaceResponseDTO.WorkspaceMemberWithTeamsDto> result =
workspaceQueryService.getWorkspaceMembers(member);

return ApiResponse.onSuccess(result);
List<WorkspaceResponseDTO.WorkspaceMemberWithTeamsDto> sorted = result.stream()
.sorted((m1, m2) -> {
if (m1.getMemberId().equals(member.getId())) return -1; //나 자신은 항상 상단
if (m2.getMemberId().equals(member.getId())) return 1;
return m1.getJoinedAt().compareTo(m2.getJoinedAt()); //참여일 순
})
.toList();

return ApiResponse.onSuccess(sorted);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,6 @@ public void updateTeamOrder(WorkSpace workspace, List<Long> teamIdList) {

@Override
public WorkspaceResponseDTO.CreateWorkspaceResponseDto createWorkspace(Member member, WorkspaceRequestDTO.WorkspaceRequestDto request) {
// 1. 워크스페이스 이름 중복 검사
if (workspaceRepository.existsByName(request.getWorkspaceName())) {
throw new WorkspaceHandler(WorkspaceErrorStatus._DUPLICATE_WORKSPACE_NAME);
}
// 2. 이미 워크스페이스가 있으면 예외
if (member.getWorkSpace() != null) {
throw new WorkspaceHandler(WorkspaceErrorStatus._WORKSPACE_DUPLICATED);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import com.example.Veco.global.auth.jwt.util.JwtUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -53,6 +52,7 @@ public ApiResponse<TokenDTO> reissue(HttpServletRequest request, HttpServletResp
ResponseCookie refreshTokenCookie = jwtUtil.createRefreshTokenCookie(newRefreshToken);
response.addHeader("Set-Cookie", refreshTokenCookie.toString());
}
log.info("토큰 재발급 성공: {}", newAccessToken);
return ApiResponse.onSuccess(TokenConverter.toTokenDTO(newAccessToken));
} catch (CustomJwtException e) {
log.error("토큰 재발급 실패: {}", e.getMessage());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.example.Veco.global.auth.jwt.filter;

import com.example.Veco.global.apiPayload.ApiResponse;
import com.example.Veco.global.auth.jwt.exception.CustomJwtException;
import com.example.Veco.global.auth.jwt.exception.code.JwtErrorCode;
import com.example.Veco.global.auth.jwt.util.JwtUtil;
import com.example.Veco.global.auth.user.exception.UserException;
import com.example.Veco.global.auth.user.exception.code.UserErrorCode;
import com.example.Veco.global.auth.user.service.CustomUserDetailsService;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import jakarta.servlet.FilterChain;
Expand All @@ -15,6 +17,7 @@
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
Expand All @@ -23,14 +26,17 @@
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Map;

@Slf4j
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {

private final JwtUtil jwtUtil;
private final CustomUserDetailsService customUserDetailsService;
private final ObjectMapper objectMapper = new ObjectMapper();

private static final AntPathMatcher pathMatcher = new AntPathMatcher();
private static final String[] excludePaths = {"/healthcheck", "/api/test/login/", "/api/token/reissue", "/login-test.html",
Expand Down Expand Up @@ -62,7 +68,15 @@ protected void doFilterInternal(
// Bearer이면 추출
token = token.replace("Bearer ", "");
// AccessToken 검증하기: 올바른 토큰이면
if (jwtUtil.isAccessTokenValid(token)) {
if (!jwtUtil.isAccessTokenValid(token)) {
log.error("JWT 토큰이 유효하지 않습니다: {}", token);
sendErrorResponse(response, JwtErrorCode.JWT_INVALID_TOKEN);
return;
} else if (jwtUtil.isTokenExpired(token)) {
log.error("JWT 토큰이 만료되었습니다: {}", token);
sendErrorResponse(response, JwtErrorCode.JWT_EXPIRED_TOKEN);
return;
} else {
// 토큰에서 이메일 추출
String uid = jwtUtil.getUsername(token);
UserDetails user = customUserDetailsService.loadUserByUsername(uid);
Expand All @@ -76,11 +90,27 @@ protected void doFilterInternal(
}
filterChain.doFilter(request, response);
} catch (ExpiredJwtException e) {
throw new CustomJwtException(JwtErrorCode.JWT_EXPIRED_TOKEN);
log.error("JWT 토큰 만료: {}", e.getMessage());
sendErrorResponse(response, JwtErrorCode.JWT_EXPIRED_TOKEN);
} catch (MalformedJwtException e) {
throw new CustomJwtException(JwtErrorCode.JWT_MALFORMED_TOKEN);
log.error("JWT 형식 오류: {}", e.getMessage());
sendErrorResponse(response, JwtErrorCode.JWT_MALFORMED_TOKEN);
} catch (UserException e) {
throw new UserException(UserErrorCode.USER_NOT_FOUND);
}
}

private void sendErrorResponse(HttpServletResponse response, JwtErrorCode errorCode) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(errorCode.getHttpStatus().value());

objectMapper.writeValue(
response.getOutputStream(),
ApiResponse.onFailure(
errorCode.getCode(),
errorCode.getMessage(),
null
)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,13 @@ public boolean isAccessTokenValid(String token) {
try {
// 토큰이 블랙리스트에 존재하는지 확인
if (isBlackList(token)){
log.info("토큰이 블랙리스트에 존재합니다: {}", token);
return false;
}
getClaims(token);
return true;
} catch (CustomJwtException e) {
log.error("토큰 유효성 검사 실패: {}", e.getMessage());
return false;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public void onAuthenticationSuccess(
memberCommandService.saveMember(member);

// 로딩 화면으로 리다이렉트
redirectURL = UriComponentsBuilder.fromUriString("https://web.vecoservice.shop/onboarding/loading")
redirectURL = UriComponentsBuilder.fromUriString("http://localhost:5173/onboarding/loading")
.build()
.encode(StandardCharsets.UTF_8)
.toUriString();
Expand Down
40 changes: 21 additions & 19 deletions src/main/java/com/example/Veco/global/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,19 @@
import com.example.Veco.global.auth.jwt.filter.JwtAuthFilter;
import com.example.Veco.global.auth.jwt.util.JwtUtil;
import com.example.Veco.global.auth.oauth2.handler.OAuth2SuccessHandler;
import com.example.Veco.global.auth.oauth2.resolver.CustomAuthorizationRequestResolver;
import com.example.Veco.global.auth.oauth2.service.OAuth2UserService;
import com.example.Veco.global.auth.user.service.CustomUserDetailsService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfigurationSource;
Expand All @@ -31,42 +29,46 @@ public class SecurityConfig {
private final CustomUserDetailsService customUserDetailsService;
private final OAuth2UserService customOAuth2UserService;
private final OAuth2SuccessHandler oAuth2SuccessHandler;
private final CustomAuthorizationRequestResolver customAuthorizationRequestResolver;
private final JwtUtil jwtUtil;
private final CorsConfigurationSource corsConfigurationSource;

@Bean
protected SecurityFilterChain configure(HttpSecurity http) throws Exception {
@Order(1)
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**", "/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**")
.cors(cors -> cors.configurationSource(corsConfigurationSource))
.csrf(AbstractHttpConfigurer::disable)
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/test/**", "/api/teams/*/externals/**", "/api/teams/*/externals", "/api/test/login", "/api/token/reissue", "/login-test.html", "/v3/api-docs/**", "/swagger-ui/**",
"/swagger-resources/**", "/css/**", "/images/**", "/healthcheck",
"/js/**", "/h2-console/**", "/profile","/workspace/create-url","/slack/callback", "/github/**","/api/github/**").permitAll()
.requestMatchers("/api/test/**", "/api/token/reissue",
"/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**").permitAll()
.anyRequest().authenticated()
)
.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class)
.logout(logout -> logout.logoutSuccessUrl("https://web.vecoservice.shop/onboarding"))
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

return http.build();
}

// 웹용 SecurityFilterChain (OAuth2만)
@Bean
@Order(2)
public SecurityFilterChain webFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/healthcheck", "/css/**", "/js/**", "/images/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.authorizationEndpoint(config -> config
.authorizationRequestRepository(new HttpSessionOAuth2AuthorizationRequestRepository())
.authorizationRequestResolver(customAuthorizationRequestResolver)
)
.loginPage("https://web.vecoservice.shop/onboarding")
.successHandler(oAuth2SuccessHandler)
.userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService))
.failureHandler((request, response, exception) -> {
// 로그인 실패 시 처리 로직
log.error("OAuth2 로그인 실패: {}", exception.getMessage());
response.sendRedirect("https://web.vecoservice.shop/onboarding");
})
);

return http.build();
}

Expand Down