diff --git a/SPMS.API/Controllers/ServiceController.cs b/SPMS.API/Controllers/ServiceController.cs index 78c1b64..367ed52 100644 --- a/SPMS.API/Controllers/ServiceController.cs +++ b/SPMS.API/Controllers/ServiceController.cs @@ -125,4 +125,52 @@ public class ServiceController : ControllerBase var result = await _serviceManagementService.GetCredentialsAsync(serviceCode); return Ok(ApiResponse.Success(result)); } + + [HttpPost("{serviceCode}/ip/list")] + [SwaggerOperation( + Summary = "IP 화이트리스트 조회", + Description = "서비스에 등록된 IP 화이트리스트 목록을 조회합니다.")] + [SwaggerResponse(200, "조회 성공", typeof(ApiResponse))] + [SwaggerResponse(401, "인증되지 않은 요청")] + [SwaggerResponse(403, "권한 없음")] + [SwaggerResponse(404, "서비스를 찾을 수 없음")] + public async Task GetIpListAsync([FromRoute] string serviceCode) + { + var result = await _serviceManagementService.GetIpListAsync(serviceCode); + return Ok(ApiResponse.Success(result)); + } + + [HttpPost("{serviceCode}/ip/add")] + [SwaggerOperation( + Summary = "IP 추가", + Description = "서비스의 IP 화이트리스트에 새 IP를 추가합니다. IPv4 형식만 지원합니다.")] + [SwaggerResponse(200, "추가 성공", typeof(ApiResponse))] + [SwaggerResponse(400, "잘못된 요청 (유효하지 않은 IP 형식)")] + [SwaggerResponse(401, "인증되지 않은 요청")] + [SwaggerResponse(403, "권한 없음")] + [SwaggerResponse(404, "서비스를 찾을 수 없음")] + [SwaggerResponse(409, "이미 등록된 IP")] + public async Task AddIpAsync( + [FromRoute] string serviceCode, + [FromBody] AddIpRequestDto request) + { + var result = await _serviceManagementService.AddIpAsync(serviceCode, request); + return Ok(ApiResponse.Success(result)); + } + + [HttpPost("{serviceCode}/ip/delete")] + [SwaggerOperation( + Summary = "IP 삭제", + Description = "서비스의 IP 화이트리스트에서 IP를 삭제합니다.")] + [SwaggerResponse(200, "삭제 성공", typeof(ApiResponse))] + [SwaggerResponse(401, "인증되지 않은 요청")] + [SwaggerResponse(403, "권한 없음")] + [SwaggerResponse(404, "서비스 또는 IP를 찾을 수 없음")] + public async Task DeleteIpAsync( + [FromRoute] string serviceCode, + [FromBody] DeleteIpRequestDto request) + { + await _serviceManagementService.DeleteIpAsync(serviceCode, request); + return Ok(ApiResponse.Success()); + } } diff --git a/SPMS.Application/DTOs/Service/AddIpRequestDto.cs b/SPMS.Application/DTOs/Service/AddIpRequestDto.cs new file mode 100644 index 0000000..49f2f97 --- /dev/null +++ b/SPMS.Application/DTOs/Service/AddIpRequestDto.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace SPMS.Application.DTOs.Service; + +public class AddIpRequestDto +{ + [Required(ErrorMessage = "IP 주소는 필수입니다.")] + [RegularExpression( + @"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", + ErrorMessage = "유효한 IPv4 주소 형식이 아닙니다.")] + public string IpAddress { get; set; } = string.Empty; +} diff --git a/SPMS.Application/DTOs/Service/DeleteIpRequestDto.cs b/SPMS.Application/DTOs/Service/DeleteIpRequestDto.cs new file mode 100644 index 0000000..4395049 --- /dev/null +++ b/SPMS.Application/DTOs/Service/DeleteIpRequestDto.cs @@ -0,0 +1,9 @@ +using System.ComponentModel.DataAnnotations; + +namespace SPMS.Application.DTOs.Service; + +public class DeleteIpRequestDto +{ + [Required(ErrorMessage = "IP ID는 필수입니다.")] + public long IpId { get; set; } +} diff --git a/SPMS.Application/DTOs/Service/IpListResponseDto.cs b/SPMS.Application/DTOs/Service/IpListResponseDto.cs new file mode 100644 index 0000000..01e2026 --- /dev/null +++ b/SPMS.Application/DTOs/Service/IpListResponseDto.cs @@ -0,0 +1,14 @@ +namespace SPMS.Application.DTOs.Service; + +public class IpListResponseDto +{ + public string ServiceCode { get; set; } = string.Empty; + public List Items { get; set; } = new(); + public int TotalCount { get; set; } +} + +public class ServiceIpDto +{ + public long Id { get; set; } + public string IpAddress { get; set; } = string.Empty; +} diff --git a/SPMS.Application/Interfaces/IServiceManagementService.cs b/SPMS.Application/Interfaces/IServiceManagementService.cs index 8715a71..50984dc 100644 --- a/SPMS.Application/Interfaces/IServiceManagementService.cs +++ b/SPMS.Application/Interfaces/IServiceManagementService.cs @@ -11,4 +11,9 @@ public interface IServiceManagementService Task RegisterApnsCredentialsAsync(string serviceCode, ApnsCredentialsRequestDto request); Task RegisterFcmCredentialsAsync(string serviceCode, FcmCredentialsRequestDto request); Task GetCredentialsAsync(string serviceCode); + + // IP Whitelist + Task GetIpListAsync(string serviceCode); + Task AddIpAsync(string serviceCode, AddIpRequestDto request); + Task DeleteIpAsync(string serviceCode, DeleteIpRequestDto request); } diff --git a/SPMS.Application/Services/ServiceManagementService.cs b/SPMS.Application/Services/ServiceManagementService.cs index 0a20e49..ceacc22 100644 --- a/SPMS.Application/Services/ServiceManagementService.cs +++ b/SPMS.Application/Services/ServiceManagementService.cs @@ -279,6 +279,90 @@ public class ServiceManagementService : IServiceManagementService return response; } + public async Task GetIpListAsync(string serviceCode) + { + var service = await _serviceRepository.GetByServiceCodeWithIpsAsync(serviceCode); + if (service is null) + { + throw new SpmsException( + ErrorCodes.NotFound, + "서비스를 찾을 수 없습니다.", + 404); + } + + return new IpListResponseDto + { + ServiceCode = service.ServiceCode, + Items = service.ServiceIps.Select(ip => new ServiceIpDto + { + Id = ip.Id, + IpAddress = ip.IpAddress + }).ToList(), + TotalCount = service.ServiceIps.Count + }; + } + + public async Task AddIpAsync(string serviceCode, AddIpRequestDto request) + { + var service = await _serviceRepository.GetByServiceCodeAsync(serviceCode); + if (service is null) + { + throw new SpmsException( + ErrorCodes.NotFound, + "서비스를 찾을 수 없습니다.", + 404); + } + + // Check for duplicate IP + var exists = await _serviceRepository.ServiceIpExistsAsync(service.Id, request.IpAddress); + if (exists) + { + throw new SpmsException( + ErrorCodes.Conflict, + "이미 등록된 IP 주소입니다.", + 409); + } + + var serviceIp = new ServiceIp + { + ServiceId = service.Id, + IpAddress = request.IpAddress + }; + + await _serviceRepository.AddServiceIpAsync(serviceIp); + await _unitOfWork.SaveChangesAsync(); + + return new ServiceIpDto + { + Id = serviceIp.Id, + IpAddress = serviceIp.IpAddress + }; + } + + public async Task DeleteIpAsync(string serviceCode, DeleteIpRequestDto request) + { + var service = await _serviceRepository.GetByServiceCodeAsync(serviceCode); + if (service is null) + { + throw new SpmsException( + ErrorCodes.NotFound, + "서비스를 찾을 수 없습니다.", + 404); + } + + var serviceIp = await _serviceRepository.GetServiceIpByIdAsync(request.IpId); + if (serviceIp is null || serviceIp.ServiceId != service.Id) + { + throw new SpmsException( + ErrorCodes.NotFound, + "IP 주소를 찾을 수 없습니다.", + 404); + } + + _serviceRepository.DeleteServiceIp(serviceIp); + await _unitOfWork.SaveChangesAsync(); + } + private static ServiceSummaryDto MapToSummaryDto(Service service) { return new ServiceSummaryDto diff --git a/SPMS.Domain/Interfaces/IServiceRepository.cs b/SPMS.Domain/Interfaces/IServiceRepository.cs index 795dce8..dab4cfb 100644 --- a/SPMS.Domain/Interfaces/IServiceRepository.cs +++ b/SPMS.Domain/Interfaces/IServiceRepository.cs @@ -8,5 +8,12 @@ public interface IServiceRepository : IRepository Task GetByServiceCodeAsync(string serviceCode); Task GetByApiKeyAsync(string apiKey); Task GetByIdWithIpsAsync(long id); + Task GetByServiceCodeWithIpsAsync(string serviceCode); Task> GetByStatusAsync(ServiceStatus status); + + // ServiceIp methods + Task GetServiceIpByIdAsync(long ipId); + Task ServiceIpExistsAsync(long serviceId, string ipAddress); + Task AddServiceIpAsync(ServiceIp serviceIp); + void DeleteServiceIp(ServiceIp serviceIp); } diff --git a/SPMS.Infrastructure/Persistence/Repositories/ServiceRepository.cs b/SPMS.Infrastructure/Persistence/Repositories/ServiceRepository.cs index d346ba9..35180ab 100644 --- a/SPMS.Infrastructure/Persistence/Repositories/ServiceRepository.cs +++ b/SPMS.Infrastructure/Persistence/Repositories/ServiceRepository.cs @@ -20,6 +20,25 @@ public class ServiceRepository : Repository, IServiceRepository .Include(s => s.ServiceIps) .FirstOrDefaultAsync(s => s.Id == id); + public async Task GetByServiceCodeWithIpsAsync(string serviceCode) + => await _dbSet + .Include(s => s.ServiceIps) + .FirstOrDefaultAsync(s => s.ServiceCode == serviceCode); + public async Task> GetByStatusAsync(ServiceStatus status) => await _dbSet.Where(s => s.Status == status).ToListAsync(); + + // ServiceIp methods + public async Task GetServiceIpByIdAsync(long ipId) + => await _context.Set().FirstOrDefaultAsync(ip => ip.Id == ipId); + + public async Task ServiceIpExistsAsync(long serviceId, string ipAddress) + => await _context.Set() + .AnyAsync(ip => ip.ServiceId == serviceId && ip.IpAddress == ipAddress); + + public async Task AddServiceIpAsync(ServiceIp serviceIp) + => await _context.Set().AddAsync(serviceIp); + + public void DeleteServiceIp(ServiceIp serviceIp) + => _context.Set().Remove(serviceIp); }