improvement: API Key 마스킹 및 전체 조회 엔드포인트 추가 (#220)

- 상세 조회 시 API Key 마스킹 (앞 8자 + ********)
- API Key 전체 조회 엔드포인트 신규 (apikey/view)
- 기존 재발급 엔드포인트 (apikey/refresh) 유지
- Swagger Description 업데이트

Closes #220
This commit is contained in:
SEAN 2026-02-25 13:56:59 +09:00
parent c20025e181
commit 351135549e
3 changed files with 50 additions and 2 deletions

View File

@ -116,7 +116,7 @@ public class ServiceController : ControllerBase
[HttpPost("{serviceCode}")] [HttpPost("{serviceCode}")]
[SwaggerOperation( [SwaggerOperation(
Summary = "서비스 상세 조회", 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<ServiceResponseDto>))] [SwaggerResponse(200, "조회 성공", typeof(ApiResponse<ServiceResponseDto>))]
[SwaggerResponse(401, "인증되지 않은 요청")] [SwaggerResponse(401, "인증되지 않은 요청")]
[SwaggerResponse(403, "권한 없음")] [SwaggerResponse(403, "권한 없음")]
@ -144,6 +144,20 @@ public class ServiceController : ControllerBase
return Ok(ApiResponse<ServiceResponseDto>.Success(result)); return Ok(ApiResponse<ServiceResponseDto>.Success(result));
} }
[HttpPost("{serviceCode}/apikey/view")]
[SwaggerOperation(
Summary = "API Key 전체 조회",
Description = "서비스의 API Key 전체 값을 조회합니다. 상세 조회 시 마스킹된 키 대신 전체 키를 확인할 때 사용합니다. 키 회전(재발급) 없이 현재 키를 반환합니다.")]
[SwaggerResponse(200, "조회 성공", typeof(ApiResponse<ApiKeyRefreshResponseDto>))]
[SwaggerResponse(401, "인증되지 않은 요청")]
[SwaggerResponse(403, "권한 없음")]
[SwaggerResponse(404, "서비스를 찾을 수 없음")]
public async Task<IActionResult> ViewApiKeyAsync([FromRoute] string serviceCode)
{
var result = await _serviceManagementService.ViewApiKeyAsync(serviceCode);
return Ok(ApiResponse<ApiKeyRefreshResponseDto>.Success(result));
}
[HttpPost("{serviceCode}/apikey/refresh")] [HttpPost("{serviceCode}/apikey/refresh")]
[SwaggerOperation( [SwaggerOperation(
Summary = "API Key 재발급", Summary = "API Key 재발급",

View File

@ -12,6 +12,7 @@ public interface IServiceManagementService
Task<ServiceResponseDto> GetByServiceCodeAsync(string serviceCode); Task<ServiceResponseDto> GetByServiceCodeAsync(string serviceCode);
Task DeleteAsync(DeleteServiceRequestDto request); Task DeleteAsync(DeleteServiceRequestDto request);
Task<ServiceResponseDto> ChangeStatusAsync(string serviceCode, ChangeServiceStatusRequestDto request); Task<ServiceResponseDto> ChangeStatusAsync(string serviceCode, ChangeServiceStatusRequestDto request);
Task<ApiKeyRefreshResponseDto> ViewApiKeyAsync(string serviceCode);
Task<ApiKeyRefreshResponseDto> RefreshApiKeyAsync(string serviceCode); Task<ApiKeyRefreshResponseDto> RefreshApiKeyAsync(string serviceCode);
Task RegisterApnsCredentialsAsync(string serviceCode, ApnsCredentialsRequestDto request); Task RegisterApnsCredentialsAsync(string serviceCode, ApnsCredentialsRequestDto request);
Task RegisterFcmCredentialsAsync(string serviceCode, FcmCredentialsRequestDto request); Task RegisterFcmCredentialsAsync(string serviceCode, FcmCredentialsRequestDto request);

View File

@ -314,6 +314,25 @@ public class ServiceManagementService : IServiceManagementService
return MapToDto(service); return MapToDto(service);
} }
public async Task<ApiKeyRefreshResponseDto> 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<ApiKeyRefreshResponseDto> RefreshApiKeyAsync(string serviceCode) public async Task<ApiKeyRefreshResponseDto> RefreshApiKeyAsync(string serviceCode)
{ {
var service = await _serviceRepository.GetByServiceCodeAsync(serviceCode); var service = await _serviceRepository.GetByServiceCodeAsync(serviceCode);
@ -981,7 +1000,7 @@ public class ServiceManagementService : IServiceManagementService
ServiceCode = service.ServiceCode, ServiceCode = service.ServiceCode,
ServiceName = service.ServiceName, ServiceName = service.ServiceName,
Description = service.Description, Description = service.Description,
ApiKey = service.ApiKey, ApiKey = MaskApiKey(service.ApiKey),
ApiKeyCreatedAt = service.ApiKeyCreatedAt, ApiKeyCreatedAt = service.ApiKeyCreatedAt,
ApnsBundleId = service.ApnsBundleId, ApnsBundleId = service.ApnsBundleId,
ApnsKeyId = service.ApnsKeyId, ApnsKeyId = service.ApnsKeyId,
@ -1003,6 +1022,20 @@ public class ServiceManagementService : IServiceManagementService
}; };
} }
/// <summary>
/// API Key를 마스킹합니다 (앞 8자 노출 + ********).
/// </summary>
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] + "********";
}
/// <summary> /// <summary>
/// 서비스의 플랫폼 자격증명 상태를 판정하여 PlatformSummaryDto를 반환합니다. /// 서비스의 플랫폼 자격증명 상태를 판정하여 PlatformSummaryDto를 반환합니다.
/// Android: FcmCredentials 유무로 판정 /// Android: FcmCredentials 유무로 판정