commit
This commit is contained in:
@@ -30,7 +30,9 @@ public class PostCreateBatch extends AbstractBatchTask {
|
||||
|
||||
@Autowired
|
||||
@Override
|
||||
public void setTransactionManager(@Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER) PlatformTransactionManager transactionManager) {
|
||||
public void setTransactionManager(
|
||||
@Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER) PlatformTransactionManager transactionManager
|
||||
) {
|
||||
super.setTransactionManager(transactionManager);
|
||||
}
|
||||
|
||||
|
||||
@@ -42,10 +42,13 @@ import lombok.extern.slf4j.Slf4j;
|
||||
public class PostCreateBatchChunk {
|
||||
|
||||
private final JobRepository jobRepository;
|
||||
|
||||
@Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER)
|
||||
private final PlatformTransactionManager transactionManager;
|
||||
|
||||
@Qualifier(SecondaryJpaConfig.ENTITY_MANAGER_FACTORY)
|
||||
private final EntityManagerFactory entityManagerFactory;
|
||||
|
||||
private final PostRepository postRepository;
|
||||
private final PostBackUpRepository postBackUpRepository;
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
|
||||
import org.springframework.core.type.classreading.MetadataReader;
|
||||
import org.springframework.core.type.classreading.MetadataReaderFactory;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.spring.infra.db.orm.jpa.annotation.DatabaseSelector;
|
||||
|
||||
@@ -48,7 +47,7 @@ public class EntityScanner {
|
||||
if (!reader.getAnnotationMetadata().hasAnnotation(Entity.class.getName())) {
|
||||
return false;
|
||||
}
|
||||
if (StringUtils.hasText(dbName)) {
|
||||
if (dbName != null) {
|
||||
var attributes = reader.getAnnotationMetadata().getAnnotationAttributes(DatabaseSelector.class.getName());
|
||||
return attributes != null && dbName.equals(attributes.get("value"));
|
||||
}
|
||||
|
||||
@@ -51,7 +51,6 @@ public class SecurityConfig {
|
||||
"/",
|
||||
"/h2-console/**",
|
||||
"/favicon.ico",
|
||||
"/sign-up",
|
||||
"/api/user/sign-up"
|
||||
};
|
||||
|
||||
@@ -95,8 +94,9 @@ public class SecurityConfig {
|
||||
.addFilterAfter(
|
||||
new JwtAuthenticationFilter(tokenService, List.of(PERMITTED_URI)),
|
||||
AuthenticationProcessingFilter.class
|
||||
).addFilterAfter(
|
||||
new RedirectIfAuthenticatedFilter(),
|
||||
)
|
||||
.addFilterAfter(
|
||||
new RedirectIfAuthenticatedFilter(),
|
||||
JwtAuthenticationFilter.class
|
||||
)
|
||||
.exceptionHandling(ex -> ex
|
||||
|
||||
@@ -16,6 +16,7 @@ public enum SecurityExceptionRule implements ErrorRule {
|
||||
USER_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "사용자 인증에 실패 하였습니다."),
|
||||
USER_FORBIDDEN(HttpStatus.FORBIDDEN, "사용자 권한이 없습니다."),
|
||||
JWT_TOKEN_ERROR(HttpStatus.UNAUTHORIZED, "토큰이 잘못되었습니다."),
|
||||
JWT_TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "토큰 정보가 없습니다."),
|
||||
SIGNATURE_ERROR(HttpStatus.UNAUTHORIZED, "토큰이 유효하지 않습니다."),
|
||||
MALFORMED_JWT_ERROR(HttpStatus.UNAUTHORIZED, "올바르지 않은 토큰입니다."),
|
||||
EXPIRED_JWT_ERROR(HttpStatus.UNAUTHORIZED, "토큰이 만료되었습니다. 다시 로그인해주세요.");
|
||||
|
||||
@@ -55,40 +55,36 @@ public final class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
) throws ServletException, IOException {
|
||||
|
||||
String requestURI = request.getRequestURI();
|
||||
if (permitAllUrls.stream().anyMatch(url -> pathMatcher.match(url, requestURI))) {
|
||||
if (permitAllUrls.stream().anyMatch(url -> pathMatcher.match(url, requestURI)) && !"/".equals(requestURI)) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
String accessToken = jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.ACCESS_PREFIX);
|
||||
if (jwtTokenService.validateAccessToken(accessToken)) {
|
||||
setAuthenticationToContext(accessToken);
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String accessToken = jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.ACCESS_PREFIX);
|
||||
if (jwtTokenService.validateAccessToken(accessToken)) {
|
||||
setAuthenticationToContext(accessToken);
|
||||
return;
|
||||
}
|
||||
|
||||
String refreshToken = jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.REFRESH_PREFIX);
|
||||
if (StringUtils.hasText(refreshToken)) {
|
||||
if (validateToken(refreshToken, request)) {
|
||||
try {
|
||||
String refreshToken = jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.REFRESH_PREFIX);
|
||||
if (StringUtils.hasText(refreshToken)) {
|
||||
if (validateToken(refreshToken, request)) {
|
||||
Authentication authentication = jwtTokenService.getAuthentication(refreshToken);
|
||||
String reissuedAccessToken = jwtTokenService.generateAccessToken(response, authentication);
|
||||
jwtTokenService.generateRefreshToken(response, authentication);
|
||||
setAuthenticationToContext(reissuedAccessToken);
|
||||
} catch (Exception e) {
|
||||
jwtTokenService.deleteCookie(response);
|
||||
request.setAttribute(EXCEPTION_ATTRIBUTE, e);
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
} else {
|
||||
jwtTokenService.deleteCookie(response);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
jwtTokenService.deleteCookie(response);
|
||||
filterChain.doFilter(request, response);
|
||||
} else {
|
||||
request.setAttribute(EXCEPTION_ATTRIBUTE, e);
|
||||
} finally {
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,7 +23,7 @@ public class RedirectIfAuthenticatedFilter extends OncePerRequestFilter {
|
||||
String requestURI = request.getRequestURI();
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth != null && auth.isAuthenticated() && "/".equals(requestURI)) {
|
||||
response.sendRedirect("/main");
|
||||
response.sendRedirect("/dashboard");
|
||||
return;
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
|
||||
@@ -7,6 +7,8 @@ import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -35,12 +37,23 @@ public class SecurityAuthenticationEntryPoint implements AuthenticationEntryPoin
|
||||
AuthenticationException authException) throws IOException, ServletException {
|
||||
if (!endpointChecker.isEndpointExist(request)) {
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||
} else if (isApiRequest(request)) {
|
||||
handleApiRequest(request, response, authException);
|
||||
} else {
|
||||
if (request.getAttribute(EXCEPTION_ATTRIBUTE) != null) {
|
||||
resolver.resolveException(request, response, null, (Exception) request.getAttribute(EXCEPTION_ATTRIBUTE));
|
||||
} else {
|
||||
resolver.resolveException(request, response, null, authException);
|
||||
}
|
||||
response.sendRedirect("/");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isApiRequest(HttpServletRequest request) {
|
||||
String accept = request.getHeader(HttpHeaders.ACCEPT);
|
||||
return accept != null && accept.contains(MediaType.APPLICATION_JSON_VALUE);
|
||||
}
|
||||
|
||||
private void handleApiRequest(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
|
||||
if (request.getAttribute(EXCEPTION_ATTRIBUTE) != null) {
|
||||
resolver.resolveException(request, response, null, (Exception) request.getAttribute(EXCEPTION_ATTRIBUTE));
|
||||
} else {
|
||||
resolver.resolveException(request, response, null, authException);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.spring.infra.security.error.SecurityAuthException;
|
||||
import com.spring.infra.security.error.SecurityExceptionRule;
|
||||
import com.spring.infra.security.service.UserPrincipalService;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
@@ -136,12 +138,12 @@ public class JwtTokenService {
|
||||
* @param request HTTP 요청 객체
|
||||
* @param tokenPrefix 토큰 접두사
|
||||
* @return 추출된 토큰
|
||||
* @throws IllegalStateException 토큰이 없을 경우 발생
|
||||
* @throws SecurityAuthException 토큰이 없을 경우 발생
|
||||
*/
|
||||
public String resolveTokenFromCookie(HttpServletRequest request, JwtTokenRule tokenPrefix) {
|
||||
Cookie[] cookies = request.getCookies();
|
||||
if (cookies == null) {
|
||||
throw new IllegalStateException("JWT_TOKEN_NOT_FOUND");
|
||||
throw new SecurityAuthException(SecurityExceptionRule.JWT_TOKEN_NOT_FOUND);
|
||||
}
|
||||
return jwtTokenUtil.resolveTokenFromCookie(cookies, tokenPrefix);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
const baseUrl = window.BASE_URL || '';
|
||||
const timeOut = window.TIME_OUT || 5000;
|
||||
|
||||
// Axios apiClient 생성
|
||||
const apiClient = axios.create({
|
||||
baseURL: BASE_URL,
|
||||
timeout: TIME_OUT,
|
||||
baseURL: baseUrl,
|
||||
timeout: timeOut,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
// 요청 인터셉터 추가
|
||||
apiClient.interceptors.request.use(
|
||||
(config) => {
|
||||
return config;
|
||||
@@ -17,7 +19,6 @@ apiClient.interceptors.request.use(
|
||||
}
|
||||
);
|
||||
|
||||
// 응답 인터셉터 추가
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => {
|
||||
return response.data;
|
||||
|
||||
@@ -7,18 +7,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
fetchDataAndRender();
|
||||
});
|
||||
|
||||
const fetchDataAndRender = async () => {
|
||||
const [year, month] = selectedMonth.split('-');
|
||||
const batchData = await getBatchJobExecutionData(year, month);
|
||||
const recentJobs = await getRecentJobs();
|
||||
|
||||
renderBatchExecutionTimeChart(batchData.jobAvgSummary);
|
||||
renderBatchStatusChart(batchData.statusCounts);
|
||||
renderHourlyJobExecutionChart(batchData.jobHourSummary);
|
||||
renderDailyJobExecutionsChart(batchData.jobExecutionSummary);
|
||||
renderRecentJobsTable(recentJobs);
|
||||
};
|
||||
|
||||
const initMonthPicker = () => {
|
||||
const monthPicker = document.getElementById('monthPicker');
|
||||
const currentDate = dayjs();
|
||||
@@ -36,6 +24,18 @@ const initMonthPicker = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const fetchDataAndRender = async () => {
|
||||
const [year, month] = selectedMonth.split('-');
|
||||
const batchData = await getBatchJobExecutionData(year, month);
|
||||
const recentJobs = await getRecentJobs();
|
||||
|
||||
renderBatchExecutionTimeChart(batchData.jobAvgSummary);
|
||||
renderBatchStatusChart(batchData.statusCounts);
|
||||
renderHourlyJobExecutionChart(batchData.jobHourSummary);
|
||||
renderDailyJobExecutionsChart(batchData.jobExecutionSummary);
|
||||
renderRecentJobsTable(recentJobs);
|
||||
};
|
||||
|
||||
const chartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
|
||||
@@ -12,6 +12,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
signIn(username, password).then(response => {
|
||||
console.log(response);
|
||||
if (response.status) {
|
||||
window.location.href = response.redirectUrl;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
|
||||
<head th:replace="fragments/config :: config"></head>
|
||||
<body>
|
||||
<div th:replace="fragments/header::header"></div>
|
||||
<div th:replace="fragments/left::sidebar"></div>
|
||||
<div layout:fragment="content"></div>
|
||||
</body>
|
||||
<body>
|
||||
<div th:replace="fragments/header::header"></div>
|
||||
<div th:replace="fragments/left::sidebar"></div>
|
||||
<div layout:fragment="content"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,7 +3,7 @@
|
||||
xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
|
||||
<head th:replace="fragments/config :: config"/>
|
||||
<body>
|
||||
<section layout:fragment="content"></section>
|
||||
</body>
|
||||
<body>
|
||||
<section layout:fragment="content"></section>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,12 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="ko" xml:lang="ko">
|
||||
<html xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorate="~{layouts/signin-layout}" lang="ko" xml:lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Error page</title>
|
||||
<link rel="stylesheet" href="/css/error.css">
|
||||
<title>Error</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Error Page</h1>
|
||||
<span th:text="${message}"></span>
|
||||
</body>
|
||||
<body class="bg-light">
|
||||
<section layout:fragment="content">
|
||||
<div class="container min-vh-100 d-flex align-items-center justify-content-center py-5">
|
||||
<div class="card border-0 shadow-lg" style="max-width: 500px;">
|
||||
<div class="card-body text-center p-5">
|
||||
<div class="display-1 text-danger mb-4">
|
||||
<i class="bi bi-exclamation-triangle-fill"></i>
|
||||
</div>
|
||||
<h1 class="display-4 fw-bold text-danger mb-4" th:text="${#strings.substring(message, 0, 3)}">500</h1>
|
||||
<h2 class="h4 text-secondary mb-4" th:text="${#strings.substring(message, 4)}">내부 서버 오류</h2>
|
||||
<p class="text-muted mb-4">죄송합니다. 문제가 발생했습니다. 기술팀이 이 문제를 해결하기 위해 노력하고 있습니다.</p>
|
||||
<a href="/" class="btn btn-primary btn-lg d-inline-flex align-items-center">
|
||||
<i class="bi bi-house-door-fill me-2"></i>홈으로 돌아가기
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user