diff --git a/SPMS.API/Controllers/ServiceController.cs b/SPMS.API/Controllers/ServiceController.cs index fc968a7..ddb0ed0 100644 --- a/SPMS.API/Controllers/ServiceController.cs +++ b/SPMS.API/Controllers/ServiceController.cs @@ -116,7 +116,7 @@ public class ServiceController : ControllerBase [HttpPost("{serviceCode}")] [SwaggerOperation( Summary = "서비스 상세 조회", - Description = "특정 서비스의 상세 정보를 조회합니다. apnsAuthType(p8/p12)과 platforms 필드로 각 플랫폼의 자격증명 상태(credentialStatus: ok/warn/error), 만료일(expiresAt) 정보를 포함합니다.")] + Description = "특정 서비스의 상세 정보를 조회합니다. API Key는 마스킹(앞 8자+********)되어 반환되며, 전체 키 조회는 apikey/view 엔드포인트를 사용합니다. apnsAuthType(p8/p12)과 platforms 필드로 각 플랫폼의 자격증명 상태(credentialStatus: ok/warn/error), 만료일(expiresAt) 정보를 포함합니다.")] [SwaggerResponse(200, "조회 성공", typeof(ApiResponse))] [SwaggerResponse(401, "인증되지 않은 요청")] [SwaggerResponse(403, "권한 없음")] @@ -144,6 +144,20 @@ public class ServiceController : ControllerBase return Ok(ApiResponse.Success(result)); } + [HttpPost("{serviceCode}/apikey/view")] + [SwaggerOperation( + Summary = "API Key 전체 조회", + Description = "서비스의 API Key 전체 값을 조회합니다. 상세 조회 시 마스킹된 키 대신 전체 키를 확인할 때 사용합니다. 키 회전(재발급) 없이 현재 키를 반환합니다.")] + [SwaggerResponse(200, "조회 성공", typeof(ApiResponse))] + [SwaggerResponse(401, "인증되지 않은 요청")] + [SwaggerResponse(403, "권한 없음")] + [SwaggerResponse(404, "서비스를 찾을 수 없음")] + public async Task ViewApiKeyAsync([FromRoute] string serviceCode) + { + var result = await _serviceManagementService.ViewApiKeyAsync(serviceCode); + return Ok(ApiResponse.Success(result)); + } + [HttpPost("{serviceCode}/apikey/refresh")] [SwaggerOperation( Summary = "API Key 재발급", diff --git a/SPMS.Application/Interfaces/IServiceManagementService.cs b/SPMS.Application/Interfaces/IServiceManagementService.cs index cee25ab..801275d 100644 --- a/SPMS.Application/Interfaces/IServiceManagementService.cs +++ b/SPMS.Application/Interfaces/IServiceManagementService.cs @@ -12,6 +12,7 @@ public interface IServiceManagementService Task GetByServiceCodeAsync(string serviceCode); Task DeleteAsync(DeleteServiceRequestDto request); Task ChangeStatusAsync(string serviceCode, ChangeServiceStatusRequestDto request); + Task ViewApiKeyAsync(string serviceCode); Task RefreshApiKeyAsync(string serviceCode); Task RegisterApnsCredentialsAsync(string serviceCode, ApnsCredentialsRequestDto request); Task RegisterFcmCredentialsAsync(string serviceCode, FcmCredentialsRequestDto request); diff --git a/SPMS.Application/Services/ServiceManagementService.cs b/SPMS.Application/Services/ServiceManagementService.cs index 1822be2..a2eee0b 100644 --- a/SPMS.Application/Services/ServiceManagementService.cs +++ b/SPMS.Application/Services/ServiceManagementService.cs @@ -314,6 +314,25 @@ public class ServiceManagementService : IServiceManagementService return MapToDto(service); } + public async Task ViewApiKeyAsync(string serviceCode) + { + var service = await _serviceRepository.GetByServiceCodeAsync(serviceCode); + if (service is null) + { + throw new SpmsException( + ErrorCodes.NotFound, + "서비스를 찾을 수 없습니다.", + 404); + } + + return new ApiKeyRefreshResponseDto + { + ServiceCode = service.ServiceCode, + ApiKey = service.ApiKey, + ApiKeyCreatedAt = service.ApiKeyCreatedAt + }; + } + public async Task RefreshApiKeyAsync(string serviceCode) { var service = await _serviceRepository.GetByServiceCodeAsync(serviceCode); @@ -981,7 +1000,7 @@ public class ServiceManagementService : IServiceManagementService ServiceCode = service.ServiceCode, ServiceName = service.ServiceName, Description = service.Description, - ApiKey = service.ApiKey, + ApiKey = MaskApiKey(service.ApiKey), ApiKeyCreatedAt = service.ApiKeyCreatedAt, ApnsBundleId = service.ApnsBundleId, ApnsKeyId = service.ApnsKeyId, @@ -1003,6 +1022,20 @@ public class ServiceManagementService : IServiceManagementService }; } + /// + /// API Key를 마스킹합니다 (앞 8자 노출 + ********). + /// + private static string MaskApiKey(string apiKey) + { + if (string.IsNullOrEmpty(apiKey)) + return string.Empty; + + if (apiKey.Length <= 8) + return new string('*', apiKey.Length); + + return apiKey[..8] + "********"; + } + /// /// 서비스의 플랫폼 자격증명 상태를 판정하여 PlatformSummaryDto를 반환합니다. /// Android: FcmCredentials 유무로 판정