commit
This commit is contained in:
@@ -30,7 +30,9 @@ public class PostCreateBatch extends AbstractBatchTask {
|
|||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@Override
|
@Override
|
||||||
public void setTransactionManager(@Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER) PlatformTransactionManager transactionManager) {
|
public void setTransactionManager(
|
||||||
|
@Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER) PlatformTransactionManager transactionManager
|
||||||
|
) {
|
||||||
super.setTransactionManager(transactionManager);
|
super.setTransactionManager(transactionManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,10 +42,13 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
public class PostCreateBatchChunk {
|
public class PostCreateBatchChunk {
|
||||||
|
|
||||||
private final JobRepository jobRepository;
|
private final JobRepository jobRepository;
|
||||||
|
|
||||||
@Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER)
|
@Qualifier(SecondaryJpaConfig.TRANSACTION_MANAGER)
|
||||||
private final PlatformTransactionManager transactionManager;
|
private final PlatformTransactionManager transactionManager;
|
||||||
|
|
||||||
@Qualifier(SecondaryJpaConfig.ENTITY_MANAGER_FACTORY)
|
@Qualifier(SecondaryJpaConfig.ENTITY_MANAGER_FACTORY)
|
||||||
private final EntityManagerFactory entityManagerFactory;
|
private final EntityManagerFactory entityManagerFactory;
|
||||||
|
|
||||||
private final PostRepository postRepository;
|
private final PostRepository postRepository;
|
||||||
private final PostBackUpRepository postBackUpRepository;
|
private final PostBackUpRepository postBackUpRepository;
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import org.springframework.beans.factory.config.BeanDefinition;
|
|||||||
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
|
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
|
||||||
import org.springframework.core.type.classreading.MetadataReader;
|
import org.springframework.core.type.classreading.MetadataReader;
|
||||||
import org.springframework.core.type.classreading.MetadataReaderFactory;
|
import org.springframework.core.type.classreading.MetadataReaderFactory;
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
|
|
||||||
import com.spring.infra.db.orm.jpa.annotation.DatabaseSelector;
|
import com.spring.infra.db.orm.jpa.annotation.DatabaseSelector;
|
||||||
|
|
||||||
@@ -48,7 +47,7 @@ public class EntityScanner {
|
|||||||
if (!reader.getAnnotationMetadata().hasAnnotation(Entity.class.getName())) {
|
if (!reader.getAnnotationMetadata().hasAnnotation(Entity.class.getName())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (StringUtils.hasText(dbName)) {
|
if (dbName != null) {
|
||||||
var attributes = reader.getAnnotationMetadata().getAnnotationAttributes(DatabaseSelector.class.getName());
|
var attributes = reader.getAnnotationMetadata().getAnnotationAttributes(DatabaseSelector.class.getName());
|
||||||
return attributes != null && dbName.equals(attributes.get("value"));
|
return attributes != null && dbName.equals(attributes.get("value"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ public class SecurityConfig {
|
|||||||
"/",
|
"/",
|
||||||
"/h2-console/**",
|
"/h2-console/**",
|
||||||
"/favicon.ico",
|
"/favicon.ico",
|
||||||
"/sign-up",
|
|
||||||
"/api/user/sign-up"
|
"/api/user/sign-up"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -95,7 +94,8 @@ public class SecurityConfig {
|
|||||||
.addFilterAfter(
|
.addFilterAfter(
|
||||||
new JwtAuthenticationFilter(tokenService, List.of(PERMITTED_URI)),
|
new JwtAuthenticationFilter(tokenService, List.of(PERMITTED_URI)),
|
||||||
AuthenticationProcessingFilter.class
|
AuthenticationProcessingFilter.class
|
||||||
).addFilterAfter(
|
)
|
||||||
|
.addFilterAfter(
|
||||||
new RedirectIfAuthenticatedFilter(),
|
new RedirectIfAuthenticatedFilter(),
|
||||||
JwtAuthenticationFilter.class
|
JwtAuthenticationFilter.class
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public enum SecurityExceptionRule implements ErrorRule {
|
|||||||
USER_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "사용자 인증에 실패 하였습니다."),
|
USER_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "사용자 인증에 실패 하였습니다."),
|
||||||
USER_FORBIDDEN(HttpStatus.FORBIDDEN, "사용자 권한이 없습니다."),
|
USER_FORBIDDEN(HttpStatus.FORBIDDEN, "사용자 권한이 없습니다."),
|
||||||
JWT_TOKEN_ERROR(HttpStatus.UNAUTHORIZED, "토큰이 잘못되었습니다."),
|
JWT_TOKEN_ERROR(HttpStatus.UNAUTHORIZED, "토큰이 잘못되었습니다."),
|
||||||
|
JWT_TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "토큰 정보가 없습니다."),
|
||||||
SIGNATURE_ERROR(HttpStatus.UNAUTHORIZED, "토큰이 유효하지 않습니다."),
|
SIGNATURE_ERROR(HttpStatus.UNAUTHORIZED, "토큰이 유효하지 않습니다."),
|
||||||
MALFORMED_JWT_ERROR(HttpStatus.UNAUTHORIZED, "올바르지 않은 토큰입니다."),
|
MALFORMED_JWT_ERROR(HttpStatus.UNAUTHORIZED, "올바르지 않은 토큰입니다."),
|
||||||
EXPIRED_JWT_ERROR(HttpStatus.UNAUTHORIZED, "토큰이 만료되었습니다. 다시 로그인해주세요.");
|
EXPIRED_JWT_ERROR(HttpStatus.UNAUTHORIZED, "토큰이 만료되었습니다. 다시 로그인해주세요.");
|
||||||
|
|||||||
@@ -55,37 +55,33 @@ public final class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
) throws ServletException, IOException {
|
) throws ServletException, IOException {
|
||||||
|
|
||||||
String requestURI = request.getRequestURI();
|
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);
|
filterChain.doFilter(request, response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
String accessToken = jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.ACCESS_PREFIX);
|
String accessToken = jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.ACCESS_PREFIX);
|
||||||
if (jwtTokenService.validateAccessToken(accessToken)) {
|
if (jwtTokenService.validateAccessToken(accessToken)) {
|
||||||
setAuthenticationToContext(accessToken);
|
setAuthenticationToContext(accessToken);
|
||||||
filterChain.doFilter(request, response);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String refreshToken = jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.REFRESH_PREFIX);
|
String refreshToken = jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.REFRESH_PREFIX);
|
||||||
if (StringUtils.hasText(refreshToken)) {
|
if (StringUtils.hasText(refreshToken)) {
|
||||||
if (validateToken(refreshToken, request)) {
|
if (validateToken(refreshToken, request)) {
|
||||||
try {
|
|
||||||
Authentication authentication = jwtTokenService.getAuthentication(refreshToken);
|
Authentication authentication = jwtTokenService.getAuthentication(refreshToken);
|
||||||
String reissuedAccessToken = jwtTokenService.generateAccessToken(response, authentication);
|
String reissuedAccessToken = jwtTokenService.generateAccessToken(response, authentication);
|
||||||
jwtTokenService.generateRefreshToken(response, authentication);
|
jwtTokenService.generateRefreshToken(response, authentication);
|
||||||
setAuthenticationToContext(reissuedAccessToken);
|
setAuthenticationToContext(reissuedAccessToken);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
jwtTokenService.deleteCookie(response);
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
jwtTokenService.deleteCookie(response);
|
jwtTokenService.deleteCookie(response);
|
||||||
request.setAttribute(EXCEPTION_ATTRIBUTE, e);
|
request.setAttribute(EXCEPTION_ATTRIBUTE, e);
|
||||||
}
|
} finally {
|
||||||
filterChain.doFilter(request, response);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
jwtTokenService.deleteCookie(response);
|
|
||||||
filterChain.doFilter(request, response);
|
|
||||||
} else {
|
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public class RedirectIfAuthenticatedFilter extends OncePerRequestFilter {
|
|||||||
String requestURI = request.getRequestURI();
|
String requestURI = request.getRequestURI();
|
||||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||||
if (auth != null && auth.isAuthenticated() && "/".equals(requestURI)) {
|
if (auth != null && auth.isAuthenticated() && "/".equals(requestURI)) {
|
||||||
response.sendRedirect("/main");
|
response.sendRedirect("/dashboard");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import javax.servlet.http.HttpServletRequest;
|
|||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
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.core.AuthenticationException;
|
||||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@@ -35,13 +37,24 @@ public class SecurityAuthenticationEntryPoint implements AuthenticationEntryPoin
|
|||||||
AuthenticationException authException) throws IOException, ServletException {
|
AuthenticationException authException) throws IOException, ServletException {
|
||||||
if (!endpointChecker.isEndpointExist(request)) {
|
if (!endpointChecker.isEndpointExist(request)) {
|
||||||
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||||
|
} else if (isApiRequest(request)) {
|
||||||
|
handleApiRequest(request, response, authException);
|
||||||
} else {
|
} else {
|
||||||
|
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) {
|
if (request.getAttribute(EXCEPTION_ATTRIBUTE) != null) {
|
||||||
resolver.resolveException(request, response, null, (Exception) request.getAttribute(EXCEPTION_ATTRIBUTE));
|
resolver.resolveException(request, response, null, (Exception) request.getAttribute(EXCEPTION_ATTRIBUTE));
|
||||||
} else {
|
} else {
|
||||||
resolver.resolveException(request, response, null, authException);
|
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.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
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 com.spring.infra.security.service.UserPrincipalService;
|
||||||
|
|
||||||
import io.jsonwebtoken.Claims;
|
import io.jsonwebtoken.Claims;
|
||||||
@@ -136,12 +138,12 @@ public class JwtTokenService {
|
|||||||
* @param request HTTP 요청 객체
|
* @param request HTTP 요청 객체
|
||||||
* @param tokenPrefix 토큰 접두사
|
* @param tokenPrefix 토큰 접두사
|
||||||
* @return 추출된 토큰
|
* @return 추출된 토큰
|
||||||
* @throws IllegalStateException 토큰이 없을 경우 발생
|
* @throws SecurityAuthException 토큰이 없을 경우 발생
|
||||||
*/
|
*/
|
||||||
public String resolveTokenFromCookie(HttpServletRequest request, JwtTokenRule tokenPrefix) {
|
public String resolveTokenFromCookie(HttpServletRequest request, JwtTokenRule tokenPrefix) {
|
||||||
Cookie[] cookies = request.getCookies();
|
Cookie[] cookies = request.getCookies();
|
||||||
if (cookies == null) {
|
if (cookies == null) {
|
||||||
throw new IllegalStateException("JWT_TOKEN_NOT_FOUND");
|
throw new SecurityAuthException(SecurityExceptionRule.JWT_TOKEN_NOT_FOUND);
|
||||||
}
|
}
|
||||||
return jwtTokenUtil.resolveTokenFromCookie(cookies, tokenPrefix);
|
return jwtTokenUtil.resolveTokenFromCookie(cookies, tokenPrefix);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
|
const baseUrl = window.BASE_URL || '';
|
||||||
|
const timeOut = window.TIME_OUT || 5000;
|
||||||
|
|
||||||
// Axios apiClient 생성
|
// Axios apiClient 생성
|
||||||
const apiClient = axios.create({
|
const apiClient = axios.create({
|
||||||
baseURL: BASE_URL,
|
baseURL: baseUrl,
|
||||||
timeout: TIME_OUT,
|
timeout: timeOut,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 요청 인터셉터 추가
|
|
||||||
apiClient.interceptors.request.use(
|
apiClient.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
return config;
|
return config;
|
||||||
@@ -17,7 +19,6 @@ apiClient.interceptors.request.use(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// 응답 인터셉터 추가
|
|
||||||
apiClient.interceptors.response.use(
|
apiClient.interceptors.response.use(
|
||||||
(response) => {
|
(response) => {
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
@@ -7,18 +7,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
fetchDataAndRender();
|
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 initMonthPicker = () => {
|
||||||
const monthPicker = document.getElementById('monthPicker');
|
const monthPicker = document.getElementById('monthPicker');
|
||||||
const currentDate = dayjs();
|
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 = {
|
const chartOptions = {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const username = document.getElementById('username').value;
|
const username = document.getElementById('username').value;
|
||||||
const password = document.getElementById('password').value;
|
const password = document.getElementById('password').value;
|
||||||
signIn(username, password).then(response => {
|
signIn(username, password).then(response => {
|
||||||
|
console.log(response);
|
||||||
if (response.status) {
|
if (response.status) {
|
||||||
window.location.href = response.redirectUrl;
|
window.location.href = response.redirectUrl;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
xmlns:th="http://www.thymeleaf.org"
|
xmlns:th="http://www.thymeleaf.org"
|
||||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
|
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
|
||||||
<head th:replace="fragments/config :: config"></head>
|
<head th:replace="fragments/config :: config"></head>
|
||||||
<body>
|
<body>
|
||||||
<div th:replace="fragments/header::header"></div>
|
<div th:replace="fragments/header::header"></div>
|
||||||
<div th:replace="fragments/left::sidebar"></div>
|
<div th:replace="fragments/left::sidebar"></div>
|
||||||
<div layout:fragment="content"></div>
|
<div layout:fragment="content"></div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
xmlns:th="http://www.thymeleaf.org"
|
xmlns:th="http://www.thymeleaf.org"
|
||||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
|
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
|
||||||
<head th:replace="fragments/config :: config"/>
|
<head th:replace="fragments/config :: config"/>
|
||||||
<body>
|
<body>
|
||||||
<section layout:fragment="content"></section>
|
<section layout:fragment="content"></section>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -1,12 +1,27 @@
|
|||||||
<!DOCTYPE html>
|
<!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>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<title>Error</title>
|
||||||
<title>Error page</title>
|
|
||||||
<link rel="stylesheet" href="/css/error.css">
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="bg-light">
|
||||||
<h1>Error Page</h1>
|
<section layout:fragment="content">
|
||||||
<span th:text="${message}"></span>
|
<div class="container min-vh-100 d-flex align-items-center justify-content-center py-5">
|
||||||
</body>
|
<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>
|
</html>
|
||||||
Reference in New Issue
Block a user