snp-batch-validation/src/main/java/com/snp/batch/service/BypassApiAccountService.java

219 lines
9.4 KiB
Java

package com.snp.batch.service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.snp.batch.global.dto.bypass.BypassAccountResponse;
import com.snp.batch.global.dto.bypass.BypassAccountUpdateRequest;
import com.snp.batch.global.dto.bypass.ServiceIpDto;
import com.snp.batch.global.model.AccountStatus;
import com.snp.batch.global.model.BypassApiAccount;
import com.snp.batch.global.model.BypassApiServiceIp;
import com.snp.batch.global.repository.BypassApiAccountRepository;
import com.snp.batch.global.repository.BypassApiServiceIpRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.security.SecureRandom;
import java.time.LocalDate;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class BypassApiAccountService {
private static final String USERNAME_PREFIX = "bypass_";
private static final int USERNAME_RANDOM_LENGTH = 8;
private static final int PASSWORD_LENGTH = 16;
private static final String ALPHANUMERIC = "abcdefghijklmnopqrstuvwxyz0123456789";
private static final String PASSWORD_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%&*";
private static final SecureRandom RANDOM = new SecureRandom();
private final BypassApiAccountRepository accountRepository;
private final BypassApiServiceIpRepository serviceIpRepository;
private final PasswordEncoder passwordEncoder;
@Transactional
public BypassAccountResponse createAccount(String displayName, String organization,
String projectName,
String email, String phone,
LocalDate accessStartDate, LocalDate accessEndDate) {
String rawUsername = generateUsername();
String rawPassword = generatePassword();
BypassApiAccount account = BypassApiAccount.builder()
.username(rawUsername)
.passwordHash(passwordEncoder.encode(rawPassword))
.displayName(displayName)
.organization(organization)
.projectName(projectName)
.email(email)
.phone(phone)
.status(AccountStatus.ACTIVE)
.accessStartDate(accessStartDate)
.accessEndDate(accessEndDate)
.build();
BypassApiAccount saved = accountRepository.save(account);
log.info("Bypass API 계정 생성: username={}", rawUsername);
return toResponse(saved, rawPassword);
}
@Transactional(readOnly = true)
public Page<BypassAccountResponse> getAccounts(String status, int page, int size) {
PageRequest pageRequest = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt"));
Page<BypassApiAccount> accounts;
if (status != null && !status.isBlank()) {
accounts = accountRepository.findByStatus(AccountStatus.valueOf(status), pageRequest);
} else {
accounts = accountRepository.findAll(pageRequest);
}
return accounts.map(this::toResponse);
}
@Transactional(readOnly = true)
public BypassAccountResponse getAccount(Long id) {
return toResponse(findOrThrow(id));
}
@Transactional
public BypassAccountResponse updateAccount(Long id, BypassAccountUpdateRequest request) {
BypassApiAccount account = findOrThrow(id);
if (request.getDisplayName() != null) account.setDisplayName(request.getDisplayName());
if (request.getOrganization() != null) account.setOrganization(request.getOrganization());
if (request.getEmail() != null) account.setEmail(request.getEmail());
if (request.getPhone() != null) account.setPhone(request.getPhone());
if (request.getStatus() != null) account.setStatus(AccountStatus.valueOf(request.getStatus()));
if (request.getAccessStartDate() != null) account.setAccessStartDate(request.getAccessStartDate());
if (request.getAccessEndDate() != null) account.setAccessEndDate(request.getAccessEndDate());
return toResponse(accountRepository.save(account));
}
@Transactional
public void deleteAccount(Long id) {
BypassApiAccount account = findOrThrow(id);
accountRepository.delete(account);
log.info("Bypass API 계정 삭제: username={}", account.getUsername());
}
@Transactional
public BypassAccountResponse resetPassword(Long id) {
BypassApiAccount account = findOrThrow(id);
String rawPassword = generatePassword();
account.setPasswordHash(passwordEncoder.encode(rawPassword));
accountRepository.save(account);
log.info("Bypass API 비밀번호 재설정: username={}", account.getUsername());
return toResponse(account, rawPassword);
}
private BypassApiAccount findOrThrow(Long id) {
return accountRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("계정을 찾을 수 없습니다: " + id));
}
private String generateUsername() {
for (int i = 0; i < 10; i++) {
StringBuilder sb = new StringBuilder(USERNAME_PREFIX);
for (int j = 0; j < USERNAME_RANDOM_LENGTH; j++) {
sb.append(ALPHANUMERIC.charAt(RANDOM.nextInt(ALPHANUMERIC.length())));
}
String candidate = sb.toString();
if (!accountRepository.existsByUsername(candidate)) {
return candidate;
}
}
throw new IllegalStateException("Username 생성 실패: 10회 시도 초과");
}
private String generatePassword() {
StringBuilder sb = new StringBuilder(PASSWORD_LENGTH);
for (int i = 0; i < PASSWORD_LENGTH; i++) {
sb.append(PASSWORD_CHARS.charAt(RANDOM.nextInt(PASSWORD_CHARS.length())));
}
return sb.toString();
}
@Transactional(readOnly = true)
public List<ServiceIpDto> getServiceIps(Long accountId) {
findOrThrow(accountId);
return serviceIpRepository.findByAccountId(accountId).stream()
.map(ip -> ServiceIpDto.builder()
.ip(ip.getIpAddress())
.purpose(ip.getPurpose())
.description(ip.getDescription())
.expectedCallVolume(ip.getExpectedCallVolume())
.build())
.toList();
}
@Transactional
public ServiceIpDto addServiceIp(Long accountId, ServiceIpDto dto) {
BypassApiAccount account = findOrThrow(accountId);
BypassApiServiceIp saved = serviceIpRepository.save(BypassApiServiceIp.builder()
.account(account)
.ipAddress(dto.getIp())
.purpose(dto.getPurpose() != null ? dto.getPurpose() : "ETC")
.description(dto.getDescription())
.expectedCallVolume(dto.getExpectedCallVolume())
.build());
return ServiceIpDto.builder()
.ip(saved.getIpAddress())
.purpose(saved.getPurpose())
.description(saved.getDescription())
.expectedCallVolume(saved.getExpectedCallVolume())
.build();
}
@Transactional
public void deleteServiceIp(Long accountId, Long ipId) {
findOrThrow(accountId);
serviceIpRepository.deleteById(ipId);
}
private String getServiceIpsJson(Long accountId) {
List<BypassApiServiceIp> ips = serviceIpRepository.findByAccountId(accountId);
if (ips.isEmpty()) return null;
try {
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(ips.stream().map(ip ->
ServiceIpDto.builder()
.ip(ip.getIpAddress())
.purpose(ip.getPurpose())
.description(ip.getDescription())
.expectedCallVolume(ip.getExpectedCallVolume())
.build()
).toList());
} catch (Exception e) {
return null;
}
}
private BypassAccountResponse toResponse(BypassApiAccount account, String plainPassword) {
return BypassAccountResponse.builder()
.id(account.getId())
.username(account.getUsername())
.displayName(account.getDisplayName())
.organization(account.getOrganization())
.projectName(account.getProjectName())
.email(account.getEmail())
.phone(account.getPhone())
.status(account.getStatus().name())
.accessStartDate(account.getAccessStartDate())
.accessEndDate(account.getAccessEndDate())
.createdAt(account.getCreatedAt())
.updatedAt(account.getUpdatedAt())
.plainPassword(plainPassword)
.serviceIps(account.getId() != null ? getServiceIpsJson(account.getId()) : null)
.build();
}
private BypassAccountResponse toResponse(BypassApiAccount account) {
return toResponse(account, null);
}
}