From 4cb54e4c419c7abe4f85a371b729cab86e08b9ed Mon Sep 17 00:00:00 2001 From: "seonkyu.kim" Date: Tue, 10 Feb 2026 00:13:16 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20API=20Key=20=EC=9E=AC=EB=B0=9C=EA=B8=89?= =?UTF-8?q?=20API=20=EA=B5=AC=ED=98=84=20(#46)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ApiKeyRefreshResponseDto 추가 - IServiceManagementService.RefreshApiKeyAsync 인터페이스 추가 - ServiceManagementService.RefreshApiKeyAsync 구현 (32~64자 랜덤 키 생성) - ServiceController.RefreshApiKeyAsync 엔드포인트 추가 --- SPMS.API/Controllers/ServiceController.cs | 14 ++++++++ .../DTOs/Service/ApiKeyRefreshResponseDto.cs | 8 +++++ .../Interfaces/IServiceManagementService.cs | 1 + .../Services/ServiceManagementService.cs | 35 +++++++++++++++++++ 4 files changed, 58 insertions(+) create mode 100644 SPMS.Application/DTOs/Service/ApiKeyRefreshResponseDto.cs diff --git a/SPMS.API/Controllers/ServiceController.cs b/SPMS.API/Controllers/ServiceController.cs index 222af1b..7532449 100644 --- a/SPMS.API/Controllers/ServiceController.cs +++ b/SPMS.API/Controllers/ServiceController.cs @@ -63,4 +63,18 @@ public class ServiceController : ControllerBase var result = await _serviceManagementService.ChangeStatusAsync(serviceCode, request); return Ok(ApiResponse.Success(result)); } + + [HttpPost("{serviceCode}/apikey/refresh")] + [SwaggerOperation( + Summary = "API Key 재발급", + Description = "서비스의 API Key를 재발급합니다. 기존 키는 즉시 무효화되며, 새 키는 1회만 표시됩니다.")] + [SwaggerResponse(200, "재발급 성공", typeof(ApiResponse))] + [SwaggerResponse(401, "인증되지 않은 요청")] + [SwaggerResponse(403, "권한 없음")] + [SwaggerResponse(404, "서비스를 찾을 수 없음")] + public async Task RefreshApiKeyAsync([FromRoute] string serviceCode) + { + var result = await _serviceManagementService.RefreshApiKeyAsync(serviceCode); + return Ok(ApiResponse.Success(result)); + } } diff --git a/SPMS.Application/DTOs/Service/ApiKeyRefreshResponseDto.cs b/SPMS.Application/DTOs/Service/ApiKeyRefreshResponseDto.cs new file mode 100644 index 0000000..f1859af --- /dev/null +++ b/SPMS.Application/DTOs/Service/ApiKeyRefreshResponseDto.cs @@ -0,0 +1,8 @@ +namespace SPMS.Application.DTOs.Service; + +public class ApiKeyRefreshResponseDto +{ + public string ServiceCode { get; set; } = string.Empty; + public string ApiKey { get; set; } = string.Empty; + public DateTime ApiKeyCreatedAt { get; set; } +} diff --git a/SPMS.Application/Interfaces/IServiceManagementService.cs b/SPMS.Application/Interfaces/IServiceManagementService.cs index 329f60a..f7c008a 100644 --- a/SPMS.Application/Interfaces/IServiceManagementService.cs +++ b/SPMS.Application/Interfaces/IServiceManagementService.cs @@ -7,4 +7,5 @@ public interface IServiceManagementService Task GetListAsync(ServiceListRequestDto request); Task GetByServiceCodeAsync(string serviceCode); Task ChangeStatusAsync(string serviceCode, ChangeServiceStatusRequestDto request); + Task RefreshApiKeyAsync(string serviceCode); } diff --git a/SPMS.Application/Services/ServiceManagementService.cs b/SPMS.Application/Services/ServiceManagementService.cs index 80b671d..28d43ff 100644 --- a/SPMS.Application/Services/ServiceManagementService.cs +++ b/SPMS.Application/Services/ServiceManagementService.cs @@ -1,4 +1,5 @@ using System.Linq.Expressions; +using System.Security.Cryptography; using SPMS.Application.DTOs.Service; using SPMS.Application.Interfaces; using SPMS.Domain.Common; @@ -110,6 +111,40 @@ public class ServiceManagementService : IServiceManagementService return MapToDto(service); } + public async Task RefreshApiKeyAsync(string serviceCode) + { + var service = await _serviceRepository.GetByServiceCodeAsync(serviceCode); + if (service is null) + { + throw new SpmsException( + ErrorCodes.NotFound, + "서비스를 찾을 수 없습니다.", + 404); + } + + // 32~64자 랜덤 API Key 생성 (48자 = 36바이트 Base64) + var randomBytes = new byte[36]; + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(randomBytes); + } + var newApiKey = Convert.ToBase64String(randomBytes); + + service.ApiKey = newApiKey; + service.ApiKeyCreatedAt = DateTime.UtcNow; + service.UpdatedAt = DateTime.UtcNow; + + _serviceRepository.Update(service); + await _unitOfWork.SaveChangesAsync(); + + return new ApiKeyRefreshResponseDto + { + ServiceCode = service.ServiceCode, + ApiKey = newApiKey, + ApiKeyCreatedAt = service.ApiKeyCreatedAt + }; + } + private static ServiceSummaryDto MapToSummaryDto(Service service) { return new ServiceSummaryDto