- UserRoleHistoryService 추가: 역할 변경 감사 이력 서비스 구현

- `ROLE_API_KEY_REQUEST` → `ROLE_WEBHOOK` 변경: Webhook 관리 권한 분리 및 강화
- "이용자" → "개발자" 용어 통일: 템플릿, 컨트롤러 및 예외 메시지 수정
This commit is contained in:
Rinjae
2026-07-27 20:11:09 +09:00
parent 0f58f043a8
commit 2106142d93
28 changed files with 276 additions and 90 deletions
@@ -8,6 +8,7 @@ import com.eactive.apim.portal.apps.apiservice.dto.ApiGroupSearch;
import com.eactive.apim.portal.apps.apiservice.dto.ApiServiceDTO;
import com.eactive.apim.portal.apps.apiservice.service.ApiServiceService;
import com.eactive.apim.portal.common.exception.NotFoundException;
import com.eactive.apim.portal.common.util.SecurityUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -68,6 +69,10 @@ public class ApiController {
@GetMapping("/testbed/api")
public String testbedByApi(@RequestParam(value = "id", required = false) String id, Model model) {
// 테스트베드는 로그인한 사용자만 접근 가능. 미인증 시 사유와 함께 로그인 페이지로 유도.
if (!SecurityUtil.isAuthenticated()) {
return "redirect:/login?reason=auth";
}
String selectedApiServiceName = "API 서비스 선택";
String selectedServiceId = "";
boolean idExists = false;
@@ -93,6 +98,10 @@ public class ApiController {
@GetMapping("/testbed")
public String testbedByApiService(@RequestParam(value = "id", required = false) String id, Model model) {
// 테스트베드는 로그인한 사용자만 접근 가능. 미인증 시 사유와 함께 로그인 페이지로 유도.
if (!SecurityUtil.isAuthenticated()) {
return "redirect:/login?reason=auth";
}
String selectedApiServiceName = "API 서비스 선택";
boolean idExists = false;
@@ -2,6 +2,7 @@ package com.eactive.apim.portal.apps.session.service;
import com.eactive.apim.portal.apps.session.entity.UserSession;
import com.eactive.apim.portal.apps.session.repository.UserSessionRepository;
import com.eactive.apim.portal.common.util.StringMaskingUtil;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -65,7 +66,8 @@ public class UserSessionService {
.forceLogout("N")
.build();
userSessionRepository.save(session);
log.info("세션 등록 - loginId: {}, sessionId: {}, ip: {}", loginId, sessionId, ipAddress);
log.info("세션 등록 - loginId: {}, sessionId: {}, ip: {}",
StringMaskingUtil.maskLoginId(loginId), StringMaskingUtil.maskToken(sessionId), StringMaskingUtil.maskIpAddress(ipAddress));
}
/**
@@ -75,7 +77,7 @@ public class UserSessionService {
public void forceLogoutOtherSessions(String loginId, String currentSessionId) {
int count = userSessionRepository.forceLogoutOtherSessions(loginId, currentSessionId);
if (count > 0) {
log.info("강제 로그아웃 처리 - loginId: {}, 대상 세션 수: {}", loginId, count);
log.info("강제 로그아웃 처리 - loginId: {}, 대상 세션 수: {}", StringMaskingUtil.maskLoginId(loginId), count);
}
}
@@ -87,7 +89,7 @@ public class UserSessionService {
public void forceLogoutAllSessions(String loginId) {
int count = userSessionRepository.forceLogoutAllSessions(loginId);
if (count > 0) {
log.info("전체 강제 로그아웃 처리 - loginId: {}, 대상 세션 수: {}", loginId, count);
log.info("전체 강제 로그아웃 처리 - loginId: {}, 대상 세션 수: {}", StringMaskingUtil.maskLoginId(loginId), count);
}
}
@@ -108,7 +110,7 @@ public class UserSessionService {
public void removeSession(String sessionId) {
if (userSessionRepository.existsById(sessionId)) {
userSessionRepository.deleteById(sessionId);
log.debug("세션 삭제 - sessionId: {}", sessionId);
log.debug("세션 삭제 - sessionId: {}", StringMaskingUtil.maskToken(sessionId));
}
}
@@ -74,11 +74,11 @@ public class UserManRestController {
return new ResponseDTO(200, "SUCCESS", "변경되었습니다. 확인 버튼을 누르시면 계정을 로그아웃 합니다.");
}
// 관리자 -> 이용자 변경 (본인 제외)
// 관리자 -> 개발자 변경 (본인 제외)
@PostMapping("/revoke-manager")
public ResponseDTO revokeManager(@RequestBody PortalUserDTO user) {
userManFacade.revokeManager(SecurityUtil.getPortalAuthenticatedUser(), user.getId());
return new ResponseDTO(200, "SUCCESS", "이용자로 변경되었습니다.");
return new ResponseDTO(200, "SUCCESS", "개발자로 변경되었습니다.");
}
// 소속 제외 -> 개인이용자로 전환
@@ -2,6 +2,7 @@ package com.eactive.apim.portal.apps.user.facade;
import com.eactive.apim.portal.apps.agreements.service.AgreementsFacade;
import com.eactive.apim.portal.apps.user.dto.PortalUserDTO;
import com.eactive.apim.portal.common.util.StringMaskingUtil;
import com.eactive.apim.portal.apps.user.mapper.PortalUserMapper;
import com.eactive.apim.portal.apps.user.service.PasswordService;
import com.eactive.apim.portal.apps.user.service.PortalOrgService;
@@ -71,7 +72,7 @@ public class UserFacadeImpl implements UserFacade {
updateUserBasicInfo(user, portalUserDTO);
portalUserService.updateUser(user);
log.info("사용자 정보 업데이트 완료: {}", user.getLoginId());
log.info("사용자 정보 업데이트 완료: {}", StringMaskingUtil.maskLoginId(user.getLoginId()));
}
@Override
@@ -98,7 +99,7 @@ public class UserFacadeImpl implements UserFacade {
portalUserService.updateUser(user);
portalOrgService.updateOrg(org);
log.info("법인 관리자 정보 업데이트 완료: {}", user.getLoginId());
log.info("법인 관리자 정보 업데이트 완료: {}", StringMaskingUtil.maskLoginId(user.getLoginId()));
}
// 사용자 기본 업데이트
@@ -136,7 +137,7 @@ public class UserFacadeImpl implements UserFacade {
// 법인 관리자 탈퇴 제한
if (user.getRoleCode() == PortalUserEnums.RoleCode.ROLE_CORP_MANAGER) {
if(portalUserService.checkOrgHasOtherUsers(user.getPortalOrg())){
throw new IllegalArgumentException("법인 관리자권한을 다른 이용자에게 위임하신 후 탈퇴가 가능합니다.");
throw new IllegalArgumentException("법인 관리자권한을 다른 개발자에게 위임하신 후 탈퇴가 가능합니다.");
}
}
@@ -147,7 +148,7 @@ public class UserFacadeImpl implements UserFacade {
messageRequestFacade.deleteUserMessage(user.getUserName(),user.getLoginId());
portalUserService.deleteUser(user);
log.info("회원 탈퇴 처리 완료: {}", user.getLoginId());
log.info("회원 탈퇴 처리 완료: {}", StringMaskingUtil.maskLoginId(user.getLoginId()));
}
// 사용자 정보 업데이트 유효성 확인
@@ -215,6 +216,6 @@ public class UserFacadeImpl implements UserFacade {
user.setUserStatus(PortalUserEnums.UserStatus.ACTIVE);
portalUserService.save(user);
log.info("사용자 이메일 인증 완료 - 활성화: {}", email);
log.info("사용자 이메일 인증 완료 - 활성화: {}", StringMaskingUtil.maskEmail(email));
}
}
@@ -22,6 +22,8 @@ import com.eactive.apim.portal.portaluser.entity.PortalUserEnums.RoleCode;
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums.UserStatus;
import com.eactive.apim.portal.apps.session.service.UserSessionService;
import com.eactive.apim.portal.apps.user.service.PortalUserAuthService;
import com.eactive.apim.portal.apps.user.service.UserRoleHistoryService;
import com.eactive.apim.portal.apps.user.service.UserRoleHistoryService.ChangeType;
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import com.eactive.apim.portal.template.service.MessageHandlerService;
@@ -59,6 +61,7 @@ public class UserManFacade {
private final CellPhoneValidator cellPhoneValidator;
private final UserSessionService userSessionService;
private final PortalUserAuthService portalUserAuthService;
private final UserRoleHistoryService userRoleHistoryService;
public Page<PortalUserDTO> getUsers(PortalOrgDTO userOrg, Pageable pageable) {
return portalUserRepository
@@ -134,7 +137,7 @@ public class UserManFacade {
if (existingUser.isPresent()) {
PortalUser user = existingUser.get();
if (user.getPortalOrg() != null && !user.getRoleCode().equals(RoleCode.ROLE_USER)) {
throw new IllegalArgumentException("기관에 등록된 이용자 입니다.");
throw new IllegalArgumentException("기관에 등록된 개발자 입니다.");
}
}
@@ -240,6 +243,9 @@ public class UserManFacade {
PortalUser currentUser = portalUserRepository.findById(portalAuthenticatedUser.getId())
.orElseThrow(() -> new NotFoundException("사용자를 찾을 수 없습니다. "));
RoleCode targetBefore = targetUser.getRoleCode();
RoleCode currentBefore = currentUser.getRoleCode();
// 3. Assign role code ROLE_CORP_MANAGER to target User
targetUser.setRoleCode(RoleCode.ROLE_CORP_MANAGER);
@@ -249,6 +255,10 @@ public class UserManFacade {
portalUserRepository.save(targetUser);
portalUserRepository.save(currentUser);
// 역할 변경 감사 이력: 대상 위임 + 본인 회수
userRoleHistoryService.record(targetUser.getLoginId(), targetBefore, RoleCode.ROLE_CORP_MANAGER, ChangeType.MANAGER_ASSIGN);
userRoleHistoryService.record(currentUser.getLoginId(), currentBefore, RoleCode.ROLE_CORP_USER, ChangeType.MANAGER_REVOKE);
// 권한 변경 즉시 반영: 대상(타 세션)은 강제 로그아웃, 본인(현재 세션)은 in-place 재인증
userSessionService.forceLogoutAllSessions(targetUser.getLoginId());
portalUserAuthService.reloadCurrentAuthentication();
@@ -264,7 +274,7 @@ public class UserManFacade {
}
/**
* 법인 관리자 권한 회수 (관리자 → 이용자).
* 법인 관리자 권한 회수 (관리자 → 개발자).
* 대상은 본인이 아닌 같은 기관의 ROLE_CORP_MANAGER 여야 한다.
*/
public void revokeManager(PortalAuthenticatedUser admin, String targetUserId) {
@@ -279,12 +289,16 @@ public class UserManFacade {
throw new IllegalArgumentException("본인의 권한은 변경할 수 없습니다.");
}
if (targetUser.getRoleCode() != RoleCode.ROLE_CORP_MANAGER) {
throw new IllegalArgumentException("관리자만 이용자로 변경할 수 있습니다.");
throw new IllegalArgumentException("관리자만 개발자로 변경할 수 있습니다.");
}
RoleCode revokeBefore = targetUser.getRoleCode();
targetUser.setRoleCode(RoleCode.ROLE_CORP_USER);
portalUserRepository.save(targetUser);
// 역할 변경 감사 이력: 관리자 → 개발자
userRoleHistoryService.record(targetUser.getLoginId(), revokeBefore, RoleCode.ROLE_CORP_USER, ChangeType.MANAGER_REVOKE);
// 권한 회수를 대상 사용자의 활성 세션에 반영 (강제 로그아웃 → 재로그인 시 새 권한)
userSessionService.forceLogoutAllSessions(targetUser.getLoginId());
}
@@ -305,10 +319,14 @@ public class UserManFacade {
throw new IllegalArgumentException("본인은 소속에서 제외할 수 없습니다.");
}
RoleCode removeBefore = targetUser.getRoleCode();
targetUser.setPortalOrg(null);
targetUser.setRoleCode(RoleCode.ROLE_USER);
portalUserRepository.save(targetUser);
// 역할 변경 감사 이력: 소속 제외 → 개인 전환
userRoleHistoryService.record(targetUser.getLoginId(), removeBefore, RoleCode.ROLE_USER, ChangeType.ORG_REMOVE);
// 소속 제외를 대상 사용자의 활성 세션에 반영 (강제 로그아웃 → 재로그인 시 새 권한)
userSessionService.forceLogoutAllSessions(targetUser.getLoginId());
}
@@ -0,0 +1,62 @@
package com.eactive.apim.portal.apps.user.service;
import com.eactive.apim.portal.common.util.SecurityUtil;
import com.eactive.apim.portal.portaluser.entity.PortalUserEnums.RoleCode;
import com.eactive.apim.portal.portaluser.entity.UserRoleHistory;
import com.eactive.apim.portal.portaluser.repository.UserRoleHistoryRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
/**
* 사용자 역할(권한) 변경 감사 이력 기록 서비스.
*
* <p>법인 관리자 위임/회수, 소속 제외 등 역할 변경 이벤트를 {@code ptl_user_role_history} 에 남긴다.
* 변경 수행자(changedBy)는 현재 인증된 관리자의 loginId 로 기록한다.</p>
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class UserRoleHistoryService {
/** 역할 변경 유형. */
public enum ChangeType {
MANAGER_ASSIGN, // 관리자 권한 위임
MANAGER_REVOKE, // 관리자 권한 회수
ORG_REMOVE // 소속 제외(개인으로 전환)
}
private final UserRoleHistoryRepository userRoleHistoryRepository;
/**
* 역할 변경 이력을 기록한다. 감사 목적이므로 실패해도 본 트랜잭션을 롤백시키지 않도록
* 호출부에서 예외를 전파하지 않는다(내부에서 로깅만).
*/
@Transactional
public void record(String targetLoginId, RoleCode before, RoleCode after, ChangeType changeType) {
try {
String actor = SecurityUtil.getCurrentLoginId();
if (actor == null || actor.isEmpty()) {
actor = "SYSTEM";
}
LocalDateTime now = LocalDateTime.now();
UserRoleHistory history = new UserRoleHistory();
history.setUserId(targetLoginId);
history.setBeforeRole(before != null ? before.name() : null);
history.setAfterRole(after != null ? after.name() : null);
history.setChangeType(changeType.name());
history.setChangedBy(actor);
history.setChangeDate(now);
history.setCreatedBy(actor);
history.setCreatedDate(now);
userRoleHistoryRepository.save(history);
} catch (Exception e) {
log.error("역할 변경 이력 기록 실패 - target: {}, type: {}", targetLoginId, changeType, e);
}
}
}
@@ -8,6 +8,7 @@ import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import com.eactive.apim.portal.common.util.SecurityUtil;
import com.eactive.apim.portal.common.util.StringMaskingUtil;
import com.eactive.apim.portal.config.PortalProperties;
import com.eactive.apim.portal.file.exception.InvalidFileException;
import lombok.RequiredArgsConstructor;
@@ -50,16 +51,16 @@ public class PortalGlobalExceptionHandler {
@ExceptionHandler(value = UserNotLoginException.class)
public ModelAndView handleUserNotLoginException(HttpServletRequest request, UserNotLoginException ex) {
return new ModelAndView("redirect:/login");
return new ModelAndView("redirect:/login?reason=auth");
}
@ExceptionHandler(value = AccessDeniedException.class)
public ModelAndView handleAccessDeniedException(HttpServletRequest request, AccessDeniedException ex) {
// 미로그인 사용자는 로그인 페이지로 유도, 로그인 상태에서의 권한 부족은 오류 안내 페이지로 표시한다.
if (!SecurityUtil.isAuthenticated()) {
return new ModelAndView("redirect:/login");
return new ModelAndView("redirect:/login?reason=auth");
}
log.warn("접근 권한 없음: loginId={}, uri={}", SecurityUtil.getCurrentLoginId(), request.getRequestURI());
log.warn("접근 권한 없음: loginId={}, uri={}", StringMaskingUtil.maskLoginId(SecurityUtil.getCurrentLoginId()), request.getRequestURI());
ModelAndView modelAndView = new ModelAndView("error");
modelAndView.addObject("errorTitle", "페이지 접근 권한이 없습니다.");
modelAndView.addObject("errorDescription", "해당 페이지를 이용할 수 있는 권한이 없는 계정입니다.\n권한이 필요한 경우 관리자에게 문의해 주세요.");
@@ -1,5 +1,6 @@
package com.eactive.apim.portal.common.migration;
import com.eactive.apim.portal.common.util.StringMaskingUtil;
import com.eactive.apim.portal.jpa.PersonalDataEncryptConverter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -177,7 +178,8 @@ public class LegacyEncryptionMigrationController {
|| "::1".equals(remote);
boolean viaProxy = request.getHeader("X-Forwarded-For") != null;
if (!localAddr || viaProxy) {
log.warn("[마이그레이션] 비로컬 접근 차단 remoteAddr={} xff={}", remote, request.getHeader("X-Forwarded-For"));
log.warn("[마이그레이션] 비로컬 접근 차단 remoteAddr={} xff={}",
StringMaskingUtil.maskIpAddress(remote), StringMaskingUtil.maskIpAddress(request.getHeader("X-Forwarded-For")));
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "localhost(127.0.0.1) 직접 호출만 허용됩니다.");
}
}
@@ -80,6 +80,98 @@ public class StringMaskingUtil {
return number;
}
/**
* 로그인 ID 마스킹 처리 (로그 출력용).
* 이메일 형식이면 {@link #maskEmail(String)} 규칙을, 그 외(스태프 등 비이메일 ID)는 부분 마스킹을 적용한다.
*/
public static String maskLoginId(String loginId) {
if (!isValidString(loginId)) {
return loginId;
}
if (loginId.contains("@")) {
return maskEmail(loginId);
}
int len = loginId.length();
if (len <= 2) {
return stars(len);
}
if (len <= 4) {
return loginId.charAt(0) + stars(len - 1);
}
// 5자 이상: 앞2 + 마스킹 + 뒤1
return loginId.substring(0, 2) + stars(len - 3) + loginId.charAt(len - 1);
}
/**
* IP 주소 마스킹 처리 (로그 출력용). 첫 옥텟만 남기고 나머지를 마스킹한다.
* <pre>127.0.0.1 → 127.***.***.***</pre>
* IPv4 형식이 아니면 원본을 반환한다(멱등).
*/
public static String maskIpAddress(String ip) {
if (!isValidString(ip)) {
return ip;
}
String[] parts = ip.split("\\.");
if (parts.length != 4) {
return ip;
}
return parts[0] + ".***.***.***";
}
/**
* 토큰/세션ID 등 식별자 마스킹 처리 (로그 출력용). 앞 4자 + {@code ***} + 뒤 4자만 노출한다.
* 길이가 짧으면(9자 미만) 전체를 마스킹한다.
*/
public static String maskToken(String token) {
if (!isValidString(token)) {
return token;
}
int len = token.length();
if (len < 9) {
return stars(len);
}
return token.substring(0, 4) + "***" + token.substring(len - 4);
}
// 로그에 값을 그대로 남기면 안 되는 민감 헤더(소문자 비교)
private static final java.util.Set<String> SENSITIVE_HEADERS = new java.util.HashSet<>(Arrays.asList(
"cookie", "set-cookie", "authorization", "proxy-authorization",
"x-auth-token", "x-csrf-token", "x-xsrf-token", "x-api-key"));
private static final String REDACTED = "***REDACTED***";
/**
* 요청/응답 헤더 로깅 시 민감 헤더(Cookie/Authorization 등)의 값을 {@code ***REDACTED***} 로 치환한다.
* 그 외 헤더는 원본 값을 반환한다.
*/
public static String maskHeaderValue(String headerName, String headerValue) {
if (headerName != null && SENSITIVE_HEADERS.contains(headerName.toLowerCase())) {
return REDACTED;
}
return headerValue;
}
// 세션 속성 이름에 포함되면 값을 리댁트할 키워드(소문자 부분일치)
private static final String[] SENSITIVE_ATTRIBUTE_KEYWORDS = {
"token", "secret", "password", "credential", "csrf",
"security_context", "loginid", "authentication"};
/**
* 세션 속성 로깅 시 민감 속성(SPRING_SECURITY_CONTEXT, token, loginId 등)의 값을 리댁트한다.
* 속성 이름에 민감 키워드가 부분일치하면 {@code ***REDACTED***} 를 반환한다.
*/
public static String maskAttributeValue(String attrName, String value) {
if (attrName != null) {
String lower = attrName.toLowerCase();
for (String kw : SENSITIVE_ATTRIBUTE_KEYWORDS) {
if (lower.contains(kw)) {
return REDACTED;
}
}
}
return value;
}
// 기존 메서드 오버로딩 (하위 호환성)
public static String maskName(String name) {
return maskName(name, null, null);
@@ -4,6 +4,7 @@ import com.eactive.apim.portal.apps.login.constants.LoginConstants;
import com.eactive.apim.portal.apps.user.service.PortalUserLogService;
import com.eactive.apim.portal.common.exception.UserNotFoundException;
import com.eactive.apim.portal.common.util.HttpRequestUtil;
import com.eactive.apim.portal.common.util.StringMaskingUtil;
import com.eactive.apim.portal.common.util.StringRepeatUtil;
import com.eactive.apim.portal.portaluser.entity.PortalUser;
import com.eactive.apim.portal.portaluser.repository.PortalUserRepository;
@@ -83,7 +84,7 @@ public class PortalAuthenticationFailureHandler implements AuthenticationFailure
} catch (UserNotFoundException e) {
logger.error("{} login try {}", username, e.getMessage());
logger.error("{} login try {}", StringMaskingUtil.maskLoginId(username), e.getMessage());
}
}
@@ -105,18 +106,18 @@ public class PortalAuthenticationFailureHandler implements AuthenticationFailure
logMessage.append(StringRepeatUtil.repeat('=', 80)).append("\n");
logMessage.append("USER LOGIN FAILURE\n");
logMessage.append(StringRepeatUtil.repeat('=', 80)).append("\n");
logMessage.append("Username: ").append(username).append("\n");
logMessage.append("Session ID: ").append(request.getSession().getId()).append("\n");
logMessage.append("Username: ").append(StringMaskingUtil.maskLoginId(username)).append("\n");
logMessage.append("Session ID: ").append(StringMaskingUtil.maskToken(request.getSession().getId())).append("\n");
logMessage.append("Failed At: ").append(LocalDateTime.now().format(formatter)).append("\n");
logMessage.append("Failure Reason: ").append(exception.getLocalizedMessage()).append("\n");
logMessage.append("\n");
logMessage.append(StringRepeatUtil.repeat('-', 80)).append("\n");
logMessage.append("REQUEST INFORMATION\n");
logMessage.append(StringRepeatUtil.repeat('-', 80)).append("\n");
logMessage.append("Client IP Address: ").append(HttpRequestUtil.getClientIpAddress(request)).append("\n");
logMessage.append("Client IP Address: ").append(StringMaskingUtil.maskIpAddress(HttpRequestUtil.getClientIpAddress(request))).append("\n");
logMessage.append("Client Host: ").append(HttpRequestUtil.getClientHost(request)).append("\n");
logMessage.append("Is Proxied: ").append(HttpRequestUtil.isProxied(request)).append("\n");
logMessage.append("Remote Address (Direct): ").append(request.getRemoteAddr()).append("\n");
logMessage.append("Remote Address (Direct): ").append(StringMaskingUtil.maskIpAddress(request.getRemoteAddr())).append("\n");
logMessage.append("Remote Host (Direct): ").append(request.getRemoteHost()).append("\n");
logMessage.append("Request Method: ").append(request.getMethod()).append("\n");
logMessage.append("Request URI: ").append(request.getRequestURI()).append("\n");
@@ -133,7 +134,7 @@ public class PortalAuthenticationFailureHandler implements AuthenticationFailure
java.util.Enumeration<String> headerValues = request.getHeaders(headerName);
while (headerValues.hasMoreElements()) {
String headerValue = headerValues.nextElement();
logMessage.append(String.format(" %-30s : %s\n", headerName, headerValue));
logMessage.append(String.format(" %-30s : %s\n", headerName, StringMaskingUtil.maskHeaderValue(headerName, headerValue)));
}
}
@@ -4,6 +4,7 @@ import com.eactive.apim.portal.apps.session.service.UserSessionService;
import com.eactive.apim.portal.apps.user.repository.PortalOrgRepository;
import com.eactive.apim.portal.apps.user.service.PortalUserLogService;
import com.eactive.apim.portal.common.util.HttpRequestUtil;
import com.eactive.apim.portal.common.util.StringMaskingUtil;
import com.eactive.apim.portal.common.util.PhoneNumberUtil;
import com.eactive.apim.portal.common.util.StringRepeatUtil;
import com.eactive.apim.portal.invitation.entity.UserInvitation;
@@ -150,17 +151,17 @@ public class PortalAuthenticationSuccessHandler implements AuthenticationSuccess
logMessage.append(StringRepeatUtil.repeat('=', 80)).append("\n");
logMessage.append("USER LOGIN SUCCESS\n");
logMessage.append(StringRepeatUtil.repeat('=', 80)).append("\n");
logMessage.append("Username: ").append(username).append("\n");
logMessage.append("Session ID: ").append(session.getId()).append("\n");
logMessage.append("Username: ").append(StringMaskingUtil.maskLoginId(username)).append("\n");
logMessage.append("Session ID: ").append(StringMaskingUtil.maskToken(session.getId())).append("\n");
logMessage.append("Login At: ").append(LocalDateTime.now().format(formatter)).append("\n");
logMessage.append("\n");
logMessage.append(StringRepeatUtil.repeat('-', 80)).append("\n");
logMessage.append("REQUEST INFORMATION\n");
logMessage.append(StringRepeatUtil.repeat('-', 80)).append("\n");
logMessage.append("Client IP Address: ").append(HttpRequestUtil.getClientIpAddress(request)).append("\n");
logMessage.append("Client IP Address: ").append(StringMaskingUtil.maskIpAddress(HttpRequestUtil.getClientIpAddress(request))).append("\n");
logMessage.append("Client Host: ").append(HttpRequestUtil.getClientHost(request)).append("\n");
logMessage.append("Is Proxied: ").append(HttpRequestUtil.isProxied(request)).append("\n");
logMessage.append("Remote Address (Direct): ").append(request.getRemoteAddr()).append("\n");
logMessage.append("Remote Address (Direct): ").append(StringMaskingUtil.maskIpAddress(request.getRemoteAddr())).append("\n");
logMessage.append("Remote Host (Direct): ").append(request.getRemoteHost()).append("\n");
logMessage.append("Request Method: ").append(request.getMethod()).append("\n");
logMessage.append("Request URI: ").append(request.getRequestURI()).append("\n");
@@ -177,7 +178,7 @@ public class PortalAuthenticationSuccessHandler implements AuthenticationSuccess
java.util.Enumeration<String> headerValues = request.getHeaders(headerName);
while (headerValues.hasMoreElements()) {
String headerValue = headerValues.nextElement();
logMessage.append(String.format(" %-30s : %s\n", headerName, headerValue));
logMessage.append(String.format(" %-30s : %s\n", headerName, StringMaskingUtil.maskHeaderValue(headerName, headerValue)));
}
}
@@ -2,6 +2,7 @@ package com.eactive.apim.portal.config;
import com.eactive.apim.portal.apps.session.service.UserSessionService;
import com.eactive.apim.portal.common.util.HttpRequestUtil;
import com.eactive.apim.portal.common.util.StringMaskingUtil;
import com.eactive.apim.portal.common.util.StringRepeatUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -56,11 +57,11 @@ public class PortalLogoutSuccessHandler implements LogoutHandler, LogoutSuccessH
logMessage.append(StringRepeatUtil.repeat('=', 80)).append("\n");
logMessage.append("USER LOGOUT\n");
logMessage.append(StringRepeatUtil.repeat('=', 80)).append("\n");
logMessage.append("Session ID: ").append(session.getId()).append("\n");
logMessage.append("Session ID: ").append(StringMaskingUtil.maskToken(session.getId())).append("\n");
logMessage.append("Logout At: ").append(LocalDateTime.now().format(formatter)).append("\n");
if (authentication != null) {
logMessage.append("Username: ").append(authentication.getName()).append("\n");
logMessage.append("Username: ").append(StringMaskingUtil.maskLoginId(authentication.getName())).append("\n");
logMessage.append("Authenticated: ").append(authentication.isAuthenticated()).append("\n");
}
@@ -68,10 +69,10 @@ public class PortalLogoutSuccessHandler implements LogoutHandler, LogoutSuccessH
logMessage.append(StringRepeatUtil.repeat('-', 80)).append("\n");
logMessage.append("REQUEST INFORMATION\n");
logMessage.append(StringRepeatUtil.repeat('-', 80)).append("\n");
logMessage.append("Client IP Address: ").append(HttpRequestUtil.getClientIpAddress(request)).append("\n");
logMessage.append("Client IP Address: ").append(StringMaskingUtil.maskIpAddress(HttpRequestUtil.getClientIpAddress(request))).append("\n");
logMessage.append("Client Host: ").append(HttpRequestUtil.getClientHost(request)).append("\n");
logMessage.append("Is Proxied: ").append(HttpRequestUtil.isProxied(request)).append("\n");
logMessage.append("Remote Address (Direct): ").append(request.getRemoteAddr()).append("\n");
logMessage.append("Remote Address (Direct): ").append(StringMaskingUtil.maskIpAddress(request.getRemoteAddr())).append("\n");
logMessage.append("Remote Host (Direct): ").append(request.getRemoteHost()).append("\n");
logMessage.append("Request Method: ").append(request.getMethod()).append("\n");
logMessage.append("Request URI: ").append(request.getRequestURI()).append("\n");
@@ -88,7 +89,7 @@ public class PortalLogoutSuccessHandler implements LogoutHandler, LogoutSuccessH
java.util.Enumeration<String> headerValues = request.getHeaders(headerName);
while (headerValues.hasMoreElements()) {
String headerValue = headerValues.nextElement();
logMessage.append(String.format(" %-30s : %s\n", headerName, headerValue));
logMessage.append(String.format(" %-30s : %s\n", headerName, StringMaskingUtil.maskHeaderValue(headerName, headerValue)));
}
}
@@ -102,7 +103,7 @@ public class PortalLogoutSuccessHandler implements LogoutHandler, LogoutSuccessH
while (attributeNames.hasMoreElements()) {
String attrName = attributeNames.nextElement();
Object attrValue = session.getAttribute(attrName);
String valueStr = attrValue != null ? attrValue.toString() : "null";
String valueStr = StringMaskingUtil.maskAttributeValue(attrName, attrValue != null ? attrValue.toString() : "null");
if (valueStr.length() > 100) {
valueStr = valueStr.substring(0, 97) + "...";
}
@@ -1,6 +1,7 @@
package com.eactive.apim.portal.config;
import com.eactive.apim.portal.common.util.HttpRequestUtil;
import com.eactive.apim.portal.common.util.StringMaskingUtil;
import com.eactive.apim.portal.common.util.StringRepeatUtil;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
@@ -48,7 +49,7 @@ public class SessionLoggingListener implements HttpSessionListener {
logMessage.append(StringRepeatUtil.repeat('=', 80)).append("\n");
logMessage.append("NEW HTTP SESSION CREATED\n");
logMessage.append(StringRepeatUtil.repeat('=', 80)).append("\n");
logMessage.append("Session ID: ").append(session.getId()).append("\n");
logMessage.append("Session ID: ").append(StringMaskingUtil.maskToken(session.getId())).append("\n");
logMessage.append("Created At: ").append(LocalDateTime.now().format(formatter)).append("\n");
logMessage.append("Max Inactive Interval: ").append(session.getMaxInactiveInterval()).append(" seconds\n");
@@ -57,10 +58,10 @@ public class SessionLoggingListener implements HttpSessionListener {
logMessage.append(StringRepeatUtil.repeat('-', 80)).append("\n");
logMessage.append("REQUEST INFORMATION\n");
logMessage.append(StringRepeatUtil.repeat('-', 80)).append("\n");
logMessage.append("Client IP Address: ").append(HttpRequestUtil.getClientIpAddress(request)).append("\n");
logMessage.append("Client IP Address: ").append(StringMaskingUtil.maskIpAddress(HttpRequestUtil.getClientIpAddress(request))).append("\n");
logMessage.append("Client Host: ").append(HttpRequestUtil.getClientHost(request)).append("\n");
logMessage.append("Is Proxied: ").append(HttpRequestUtil.isProxied(request)).append("\n");
logMessage.append("Remote Address (Direct): ").append(request.getRemoteAddr()).append("\n");
logMessage.append("Remote Address (Direct): ").append(StringMaskingUtil.maskIpAddress(request.getRemoteAddr())).append("\n");
logMessage.append("Remote Host (Direct): ").append(request.getRemoteHost()).append("\n");
logMessage.append("Remote Port: ").append(request.getRemotePort()).append("\n");
logMessage.append("Request Method: ").append(request.getMethod()).append("\n");
@@ -84,7 +85,7 @@ public class SessionLoggingListener implements HttpSessionListener {
Enumeration<String> headerValues = request.getHeaders(headerName);
while (headerValues.hasMoreElements()) {
String headerValue = headerValues.nextElement();
logMessage.append(String.format(" %-30s : %s\n", headerName, headerValue));
logMessage.append(String.format(" %-30s : %s\n", headerName, StringMaskingUtil.maskHeaderValue(headerName, headerValue)));
}
}
} else {
@@ -106,7 +107,7 @@ public class SessionLoggingListener implements HttpSessionListener {
logMessage.append(StringRepeatUtil.repeat('=', 80)).append("\n");
logMessage.append("HTTP SESSION DESTROYED\n");
logMessage.append(StringRepeatUtil.repeat('=', 80)).append("\n");
logMessage.append("Session ID: ").append(session.getId()).append("\n");
logMessage.append("Session ID: ").append(StringMaskingUtil.maskToken(session.getId())).append("\n");
logMessage.append("Destroyed At: ").append(LocalDateTime.now().format(formatter)).append("\n");
logMessage.append("Max Inactive Interval: ").append(session.getMaxInactiveInterval()).append(" seconds\n");
@@ -121,7 +122,8 @@ public class SessionLoggingListener implements HttpSessionListener {
while (attributeNames.hasMoreElements()) {
String attrName = attributeNames.nextElement();
Object attrValue = session.getAttribute(attrName);
logMessage.append(String.format(" %-30s : %s\n", attrName, attrValue));
logMessage.append(String.format(" %-30s : %s\n", attrName,
StringMaskingUtil.maskAttributeValue(attrName, attrValue != null ? attrValue.toString() : "null")));
}
} catch (Exception e) {
logMessage.append(" Unable to retrieve session attributes\n");
@@ -40,12 +40,12 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Slf4j
@Controller
@RequestMapping("/webhook")
@Secured("ROLE_API_KEY_REQUEST")
@Secured("ROLE_WEBHOOK")
@RequiredArgsConstructor
@SessionAttributes({"webhookRegistration", "webhookModification"})
public class WebhookController {
// 클래스 기본: 법인 관리자(ROLE_API_KEY_REQUEST) 신청/수정/삭제/Secret 관리 가능.
// 조회(index) 메서드 레벨에서 ROLE_APP 으로 완화(같은 기관 일반 사용자 열람).
// Webhook 관리는 법인 관리자 전용(ROLE_WEBHOOK). 조회/신청/수정/삭제/Secret 관리 모두 관리자만 가능.
// (개발자 ROLE_CORP_USER 권한 확장과 분리하기 위해 ROLE_API_KEY_REQUEST 에서 authority 격리)
private static final int TOTAL_STEPS = 3;
@@ -66,7 +66,7 @@ public class WebhookController {
// ============================ 조회 ============================
@Secured("ROLE_APP")
@Secured("ROLE_WEBHOOK")
@GetMapping
public ModelAndView index() {
String orgId = currentOrgId();
+5 -2
View File
@@ -224,12 +224,15 @@ portal:
- ROLE_INQUIRY
- ROLE_ACCOUNT
ROLE_CORP_USER:
- ROLE_API_KEY_REQUEST
- ROLE_API_KEY_REQUEST_VIEW
- ROLE_INQUIRY
- ROLE_APP
- ROLE_ACCOUNT
ROLE_CORP_MANAGER:
- ROLE_API_KEY_REQUEST
- ROLE_API_KEY_REQUEST_VIEW
- ROLE_WEBHOOK
- ROLE_INQUIRY
- ROLE_APP
- ROLE_ACCOUNT
@@ -355,10 +358,10 @@ page:
name: "이메일 인증"
path: "/mypage/verification-email"
user_list:
name: "이용자 관리"
name: "개발자 관리"
path: "/users"
user_detail:
name: "이용자 정보"
name: "개발자 정보"
path: "/users/detail"
apikey:
name: "앱 관리"
@@ -82,6 +82,6 @@ status=Status
table.reger=Registrar
zip=Zip Code
ROLE_CORP_MANAGER=Manager
ROLE_CORP_USER=User
ROLE_CORP_USER=Developer
ROLE_USER=Individual
login.passLengthShort=Password must be over 8 characters.
@@ -82,6 +82,6 @@ status=\uC0C1\uD0DC
table.reger=\uB4F1\uB85D\uC790
zip=\uC6B0\uD3B8\uBC88\uD638
ROLE_CORP_MANAGER=\uAD00\uB9AC\uC790
ROLE_CORP_USER=\uC774\uC6A9\uC790
ROLE_CORP_USER=\uAC1C\uBC1C\uC790
ROLE_USER=\uAC1C\uC778\uC0AC\uC6A9\uC790
ConcurrentSessionControlAuthenticationStrategy.exceededAllowed=\uCD5C\uB300 \uB85C\uADF8\uC778 \uD5C8\uC6A9 \uC5F0\uACB0\uC744 \uCD08\uACFC\uD558\uC600\uC2B5\uB2C8\uB2E4.
@@ -27,6 +27,10 @@
<i class="fas fa-info-circle"></i>
<span>다른 기기 또는 브라우저에서 로그인되어 현재 세션이 종료되었습니다.</span>
</div>
<div th:if="${param.reason == 'auth'}" class="login-alert alert-info">
<i class="fas fa-info-circle"></i>
<span>인증된 사용자만 접근 가능한 페이지입니다. 로그인 후 이용해 주세요.</span>
</div>
<!-- Login Form -->
<form id="loginForm" role="form" name="loginForm" th:action="@{/actionLogin.do}" method="post" class="login-form">
@@ -37,7 +37,7 @@
<div class="org-info-notice">
<ul>
<li>법인 관리자는 API 신청 및 이용자 초대 등 모든 권한을 보유합니다.</li>
<li>법인 관리자는 API 신청 및 개발자 초대 등 모든 권한을 보유합니다.</li>
<li>법인회원 전환은 반드시 법인이메일 주소로 입력하시기 바랍니다. 개인이메일로 전환 신청시, 법인 승인이 거절 될 수 있습니다</li>
</ul>
</div>
@@ -49,7 +49,7 @@
<div class="org-info-notice">
<ul>
<li>법인관리자는 API 신청 및 이용자 초대 등 모든 권한을 보유합니다.</li>
<li>법인관리자는 API 신청 및 개발자 초대 등 모든 권한을 보유합니다.</li>
</ul>
</div>
@@ -5,14 +5,14 @@
<section layout:fragment="title">
<div class="page-title-banner">
<img th:src="@{/img/img_title_bg.png}" class="title-image">
<h1>이용자 관리</h1>
<h1>개발자 관리</h1>
</div>
</section>
<section layout:fragment="contentFragment">
<div class="org-register-container">
<div class="common-title-bar">
<h2 class="common-title">이용자 정보</h2>
<h2 class="common-title">개발자 정보</h2>
</div>
<div class="register-form-container">
@@ -23,7 +23,7 @@
<div class="search-box">
<button type="button" class="btn btn-primary" id="addUserBtn">
<i class="fas fa-plus"></i>
<span>이용자 추가</span>
<span>개발자 추가</span>
</button>
</div>
</div>
@@ -78,7 +78,7 @@
th:if="${user.userStatus.toString() == 'ACTIVE' && user.roleCode.toString() == 'ROLE_CORP_MANAGER'}"
th:data-user-id="${user.id}"
th:data-user-name="${user.maskedUserName}">
이용자로 변경
개발자로 변경
</button>
<button type="button" class="list-table-btn list-table-btn--danger remove_from_org"
th:if="${user.userStatus.toString() == 'ACTIVE' && (user.roleCode.toString() == 'ROLE_CORP_USER' || user.roleCode.toString() == 'ROLE_CORP_MANAGER')}"
@@ -121,7 +121,7 @@
th:if="${user.userStatus.toString() == 'ACTIVE' && user.roleCode.toString() == 'ROLE_CORP_MANAGER'}"
th:data-user-id="${user.id}"
th:data-user-name="${user.maskedUserName}">
이용자로 변경
개발자로 변경
</button>
<button type="button" class="dropdown-item dropdown-item--danger remove_from_org"
th:if="${user.userStatus.toString() == 'ACTIVE' && (user.roleCode.toString() == 'ROLE_CORP_USER' || user.roleCode.toString() == 'ROLE_CORP_MANAGER')}"
@@ -155,7 +155,7 @@
<div class="row-cell" data-label="휴대폰" th:text="${user.maskedMobileNumber}">
010-****-****
</div>
<div class="row-cell" data-label="권한">이용</div>
<div class="row-cell" data-label="권한">개발</div>
<div class="row-cell" data-label="계정상태">초대중</div>
<div class="row-cell row-actions">
<!-- Desktop -->
@@ -193,8 +193,8 @@
<!-- Empty State -->
<div class="user-empty-state" th:if="${(users == null or users.isEmpty()) and (pendingUsers == null or pendingUsers.isEmpty())}">
<div class="empty-icon">👥</div>
<h3>등록된 이용자가 없습니다</h3>
<p>새로운 이용자를 추가하여 시작하세요.</p>
<h3>등록된 개발자가 없습니다</h3>
<p>새로운 개발자를 추가하여 시작하세요.</p>
</div>
<!-- Pagination -->
@@ -281,9 +281,9 @@
let errorMessage;
try {
const errorResponse = JSON.parse(jqXHR.responseText);
errorMessage = errorResponse.msg || '이용자 초대 중 오류가 발생했습니다';
errorMessage = errorResponse.msg || '개발자 초대 중 오류가 발생했습니다';
} catch (e) {
errorMessage = '이용자 초대 중 오류가 발생했습니다: ' + errorThrown;
errorMessage = '개발자 초대 중 오류가 발생했습니다: ' + errorThrown;
}
customPopups.showUserInviteError(errorMessage);
});
@@ -294,7 +294,7 @@
});
}
// Add event listeners to the "이용자 추가" buttons
// Add event listeners to the "개발자 추가" buttons
const addUserBtn = document.getElementById('addUserBtn');
if (addUserBtn) {
@@ -334,14 +334,14 @@
});
});
// 관리자 -> 이용자 변경 (본인 제외)
// 관리자 -> 개발자 변경 (본인 제외)
document.querySelectorAll('.revoke_manager').forEach(button => {
button.addEventListener('click', function(event) {
const btn = event.currentTarget;
const userId = btn.dataset.userId;
const userName = btn.dataset.userName;
customPopups.showConfirm(`<strong>${userName}</strong>님을 이용자로 변경하시겠습니까?`, function(selection) {
customPopups.showConfirm(`<strong>${userName}</strong>님을 개발자로 변경하시겠습니까?`, function(selection) {
if (!selection) return;
$.ajax({
url: '/users/revoke-manager',
@@ -33,14 +33,14 @@
<h3>등록된 Webhook이 없습니다</h3>
<p>API 서비스의 점검·지연·장애 알림을 받을 Webhook을 신청해보세요.</p>
<p class="field-help" sec:authorize="!hasRole('ROLE_API_KEY_REQUEST')">
<p class="field-help" sec:authorize="!hasRole('ROLE_WEBHOOK')">
Webhook 신청은 법인 관리자만 가능합니다. 기관 관리자에게 문의하세요.
</p>
</div>
</div>
<!-- 액션 -->
<div class="s1-actions" sec:authorize="hasRole('ROLE_API_KEY_REQUEST')">
<div class="s1-actions" sec:authorize="hasRole('ROLE_WEBHOOK')">
<button type="button" class="s1-btn-next" id="requestWebhook">Webhook 신청</button>
</div>
@@ -80,7 +80,7 @@
<span class="group-label">Secret Key</span>
<div class="secret-action-row">
<div class="secret-box" id="secretMasked" th:text="*{secretMasked}">************</div>
<th:block sec:authorize="hasRole('ROLE_API_KEY_REQUEST')">
<th:block sec:authorize="hasRole('ROLE_WEBHOOK')">
<button type="button" class="btn-reveal-action" id="btnRevealSecret">조회</button>
<button type="button" class="btn-regen-action" id="btnRegenSecret">재발급</button>
</th:block>
@@ -91,7 +91,7 @@
</div>
<!-- 액션 -->
<div class="s1-actions" sec:authorize="hasRole('ROLE_API_KEY_REQUEST')">
<div class="s1-actions" sec:authorize="hasRole('ROLE_WEBHOOK')">
<button type="button" class="btn-webhook-danger-figma" id="btnDeleteWebhook">삭제</button>
<a class="s1-btn-next-figma" th:href="@{/webhook/modify/step1}">수정</a>
</div>
@@ -88,12 +88,12 @@
<button class="header-link mypage-toggle" type="button">마이페이지</button>
<div class="mypage-dropdown-menu">
<ul class="mypage-menu-list">
<li sec:authorize="hasRole('ROLE_USER_MANAGER')">
<a th:href="@{/users}"><i class="fas fa-users"></i>이용자 관리</a>
<li sec:authorize="hasRole('ROLE_CORP_MANAGER')">
<a th:href="@{/users}"><i class="fas fa-users"></i>개발자 관리</a>
</li>
<li sec:authorize="hasRole('ROLE_APP')">
<a th:href="@{/myapikey}"><i class="fas fa-key"></i>앱 관리</a>
<a th:href="@{/webhook}" sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"><i class="fas fa-bell"></i>Webhook 관리</a>
<a th:href="@{/webhook}" sec:authorize="hasRole('ROLE_WEBHOOK')"><i class="fas fa-bell"></i>Webhook 관리</a>
<a th:href="@{/statistics/api}"><i class="fas fa-chart-bar"></i>이용 통계</a>
</li>
<li><a th:href="@{/mypage}"><i class="fas fa-user-circle"></i>내 정보 관리</a></li>
@@ -248,9 +248,9 @@
</svg>
</button>
<ul class="drawer-submenu">
<li sec:authorize="hasRole('ROLE_USER_MANAGER')"><a th:href="@{/users}">이용자 관리</a></li>
<li sec:authorize="hasRole('ROLE_CORP_MANAGER')"><a th:href="@{/users}">개발자 관리</a></li>
<li sec:authorize="hasRole('ROLE_APP')"><a th:href="@{/myapikey}">앱 관리</a></li>
<li sec:authorize="hasRole('ROLE_API_KEY_REQUEST')"><a th:href="@{/webhook}">Webhook 관리</a></li>
<li sec:authorize="hasRole('ROLE_WEBHOOK')"><a th:href="@{/webhook}">Webhook 관리</a></li>
<li sec:authorize="hasRole('ROLE_APP')"><a th:href="@{/statistics/api}">이용 통계</a></li>
<li><a th:href="@{/mypage}">내 정보 관리</a></li>
<li><a th:href="@{/password/verify}">비밀번호 변경</a></li>
@@ -53,7 +53,7 @@
<a th:href="@{/users}"
th:classappend="${activeMenu == 'users'} ? 'service-nav__item--active' : ''"
class="service-nav__item"
sec:authorize="hasRole('ROLE_CORP_MANAGER')">이용자 관리</a>
sec:authorize="hasRole('ROLE_CORP_MANAGER')">개발자 관리</a>
<a th:href="@{/myapikey}"
th:classappend="${activeMenu == 'apiKey'} ? 'service-nav__item--active' : ''"
@@ -62,7 +62,7 @@
<a th:href="@{/webhook}"
th:classappend="${activeMenu == 'webhook'} ? 'service-nav__item--active' : ''"
class="service-nav__item"
sec:authorize="hasRole('ROLE_API_KEY_REQUEST')">Webhook 관리</a>
sec:authorize="hasRole('ROLE_WEBHOOK')">Webhook 관리</a>
<a th:href="@{/statistics/api}"
th:classappend="${activeMenu == 'statistics'} ? 'service-nav__item--active' : ''"
@@ -21,19 +21,6 @@
</script>
</section>
<section sec:authorize="hasAuthority('ROLE_CORP_USER')" class="content pop" th:fragment="apiKeyRequestPopup">
<script>
$(document).ready(function() {
(function($) {
$('#requestApiKey').on('click', function(event) {
event.preventDefault();
customPopups.showAlert('법인 관리자만 신청 가능합니다.');
});
})(jQuery);
});
</script>
</section>
<section class="content pop" th:fragment="apiKeyRequestPopup" th:if="${#authorization.expression('!isAuthenticated()')}">
<script>
$(document).ready(function() {
@@ -9,7 +9,7 @@
<!-- Modal Header -->
<div class="modal-header">
<h3 class="modal-title">이용자 추가</h3>
<h3 class="modal-title">개발자 추가</h3>
<button type="button" class="modal-close" id="userInvitePopupCloseButton">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
@@ -21,7 +21,7 @@
<!-- Modal Body -->
<div class="modal-body">
<p id="userInvitePopupMessage" style="text-align: center; margin-bottom: 24px; color: #64748B;">
추가할 이용자 휴대폰 번호를 입력해 주세요.
추가할 개발자 휴대폰 번호를 입력해 주세요.
</p>
<!-- Mobile Input Field -->