@@ -1,6 +1,6 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.token;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public record BearerTokenRecord(
|
||||
long keyId,
|
||||
@@ -8,12 +8,12 @@ public record BearerTokenRecord(
|
||||
String username,
|
||||
String keyPrefix,
|
||||
String keyHash,
|
||||
OffsetDateTime expiresAt,
|
||||
OffsetDateTime revokedAt,
|
||||
LocalDateTime expiresAt,
|
||||
LocalDateTime revokedAt,
|
||||
String description
|
||||
) {
|
||||
|
||||
public boolean active(OffsetDateTime now) {
|
||||
public boolean active(LocalDateTime now) {
|
||||
return revokedAt == null && expiresAt != null && expiresAt.isAfter(now);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.cloudhandson.vpdbackoffice.mapper;
|
||||
|
||||
import com.cloudhandson.vpdbackoffice.domain.token.BearerTokenRecord;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
@@ -19,7 +19,7 @@ public interface BearerTokenMapper {
|
||||
|
||||
int revokeToken(
|
||||
@Param("keyId") long keyId,
|
||||
@Param("revokedAt") OffsetDateTime revokedAt,
|
||||
@Param("revokedAt") LocalDateTime revokedAt,
|
||||
@Param("reason") String reason
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,9 @@ import com.cloudhandson.vpdbackoffice.domain.user.AppUser;
|
||||
import com.cloudhandson.vpdbackoffice.mapper.BearerTokenMapper;
|
||||
import com.cloudhandson.vpdbackoffice.mapper.UserMapper;
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -81,6 +83,9 @@ public class BearerTokenService {
|
||||
String prefix = tokenGenerator.prefix(plainToken);
|
||||
String hash = tokenHasher.sha256(plainToken);
|
||||
long keyId = tokenMapper.nextKeyId();
|
||||
LocalDateTime expiresAt = command.expiresAt()
|
||||
.atZoneSameInstant(ZoneId.systemDefault())
|
||||
.toLocalDateTime();
|
||||
|
||||
tokenMapper.insertToken(new BearerTokenRecord(
|
||||
keyId,
|
||||
@@ -88,7 +93,7 @@ public class BearerTokenService {
|
||||
user.username(),
|
||||
prefix,
|
||||
hash,
|
||||
command.expiresAt(),
|
||||
expiresAt,
|
||||
null,
|
||||
command.description()
|
||||
));
|
||||
@@ -98,7 +103,11 @@ public class BearerTokenService {
|
||||
|
||||
@Transactional
|
||||
public void revokeToken(long keyId, String reason) {
|
||||
int updated = tokenMapper.revokeToken(keyId, OffsetDateTime.now(clock), reason);
|
||||
int updated = tokenMapper.revokeToken(
|
||||
keyId,
|
||||
LocalDateTime.now(clock.withZone(ZoneId.systemDefault())),
|
||||
reason
|
||||
);
|
||||
if (updated == 0) {
|
||||
throw new AppException("회수할 활성 토큰을 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.net.URI;
|
||||
import java.time.Clock;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
@@ -71,7 +72,7 @@ public class OrdsProbeService {
|
||||
return auditAndReturn(command, ProbeResult.blocked(
|
||||
ProbeStatus.TOKEN_NOT_FOUND, "TOKEN_NOT_FOUND", "토큰을 찾을 수 없습니다."));
|
||||
}
|
||||
if (!token.active(OffsetDateTime.now(clock))) {
|
||||
if (!token.active(LocalDateTime.now(clock.withZone(ZoneId.systemDefault())))) {
|
||||
return auditAndReturn(command, ProbeResult.blocked(
|
||||
ProbeStatus.TOKEN_INACTIVE, "TOKEN_INACTIVE", "만료되었거나 회수된 토큰입니다."));
|
||||
}
|
||||
|
||||
@@ -17,13 +17,17 @@ public class AppExceptionHandler {
|
||||
|
||||
@ExceptionHandler(DataAccessException.class)
|
||||
public String handleDataAccessException(DataAccessException exception, Model model) {
|
||||
String detail = exception.getMostSpecificCause() == null
|
||||
? exception.getMessage()
|
||||
: exception.getMostSpecificCause().getMessage();
|
||||
model.addAttribute("errorTitle", "ADB 연결 설정이 필요합니다.");
|
||||
model.addAttribute("errorMessage",
|
||||
"BACKOFFICE_DB_URL, BACKOFFICE_DB_USERNAME, BACKOFFICE_DB_PASSWORD를 실제 ADB 값으로 설정하고 "
|
||||
+ "./run.sh backoffice-support를 실행한 뒤 다시 시도하세요. 상세: " + detail);
|
||||
RuntimeErrorMessage error = RuntimeErrorMessages.dataAccess(exception);
|
||||
model.addAttribute("errorTitle", error.title());
|
||||
model.addAttribute("errorMessage", error.message());
|
||||
return "error";
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public String handleUnexpectedException(Exception exception, Model model) {
|
||||
RuntimeErrorMessage error = RuntimeErrorMessages.unexpected(exception);
|
||||
model.addAttribute("errorTitle", error.title());
|
||||
model.addAttribute("errorMessage", error.message());
|
||||
return "error";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,18 +33,14 @@ public class DashboardController {
|
||||
model.addAttribute("roles", permissionService.findRoles());
|
||||
model.addAttribute("tokens", bearerTokenService.findAll());
|
||||
} catch (DataAccessException e) {
|
||||
RuntimeErrorMessage error = RuntimeErrorMessages.dataAccess(e);
|
||||
model.addAttribute("objects", List.of());
|
||||
model.addAttribute("roles", List.of());
|
||||
model.addAttribute("tokens", List.of());
|
||||
model.addAttribute("setupError", dbSetupMessage(e));
|
||||
model.addAttribute("runtimeErrorTitle", error.title());
|
||||
model.addAttribute("runtimeErrorMessage", error.message());
|
||||
model.addAttribute("showSupportCommand", error.showSupportCommand());
|
||||
}
|
||||
return "dashboard";
|
||||
}
|
||||
|
||||
private String dbSetupMessage(DataAccessException e) {
|
||||
String detail = e.getMostSpecificCause() == null ? e.getMessage() : e.getMostSpecificCause().getMessage();
|
||||
return "ADB 연결을 확인할 수 없습니다. BACKOFFICE_DB_URL, BACKOFFICE_DB_USERNAME, "
|
||||
+ "BACKOFFICE_DB_PASSWORD를 설정하고 ./run.sh backoffice-support를 먼저 실행하세요. 상세: "
|
||||
+ detail;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
public record RuntimeErrorMessage(
|
||||
String title,
|
||||
String message,
|
||||
boolean showSupportCommand
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.cloudhandson.vpdbackoffice.web;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.Locale;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
|
||||
final class RuntimeErrorMessages {
|
||||
|
||||
private RuntimeErrorMessages() {
|
||||
}
|
||||
|
||||
static RuntimeErrorMessage dataAccess(DataAccessException exception) {
|
||||
String detail = detail(exception);
|
||||
if (looksLikeConnectionProblem(exception, detail)) {
|
||||
return new RuntimeErrorMessage(
|
||||
"DB 연결 설정이 필요합니다.",
|
||||
"ADB에 연결할 수 없습니다. BACKOFFICE_DB_URL, BACKOFFICE_DB_USERNAME, "
|
||||
+ "BACKOFFICE_DB_PASSWORD를 확인하고 ./run.sh backoffice-support를 다시 실행하세요. 상세: "
|
||||
+ detail,
|
||||
true
|
||||
);
|
||||
}
|
||||
return new RuntimeErrorMessage(
|
||||
"데이터 처리 오류가 발생했습니다.",
|
||||
"요청을 처리하는 중 DB 데이터 타입, SQL, 또는 매핑 오류가 발생했습니다. "
|
||||
+ "입력값과 백오피스 테이블 스키마를 확인하세요. 상세: " + detail,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
static RuntimeErrorMessage unexpected(Exception exception) {
|
||||
return new RuntimeErrorMessage(
|
||||
"요청 처리 중 오류가 발생했습니다.",
|
||||
"예상하지 못한 오류가 발생했습니다. 입력값을 확인한 뒤 다시 시도하세요. 상세: "
|
||||
+ safeMessage(exception),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
private static boolean looksLikeConnectionProblem(DataAccessException exception, String detail) {
|
||||
String text = (exception.getMessage() + " " + detail).toLowerCase(Locale.ROOT);
|
||||
return text.contains("connection refused")
|
||||
|| text.contains("the network adapter could not establish the connection")
|
||||
|| text.contains("io error")
|
||||
|| text.contains("ora-01017")
|
||||
|| text.contains("ora-12154")
|
||||
|| text.contains("ora-12514")
|
||||
|| text.contains("ora-12541");
|
||||
}
|
||||
|
||||
private static String detail(DataAccessException exception) {
|
||||
Throwable cause = exception.getMostSpecificCause();
|
||||
if (cause instanceof SQLException sqlException) {
|
||||
return trim(sqlException.getMessage());
|
||||
}
|
||||
return trim(cause == null ? exception.getMessage() : safeMessage(cause));
|
||||
}
|
||||
|
||||
private static String safeMessage(Throwable throwable) {
|
||||
String message = throwable.getMessage();
|
||||
return trim(message == null || message.isBlank() ? throwable.getClass().getSimpleName() : message);
|
||||
}
|
||||
|
||||
private static String trim(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return "상세 메시지가 없습니다.";
|
||||
}
|
||||
String normalized = value.replaceAll("\\s+", " ").trim();
|
||||
return normalized.length() <= 300 ? normalized : normalized.substring(0, 300) + "...";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user