diff --git a/SPMS.API/Extensions/ApplicationBuilderExtensions.cs b/SPMS.API/Extensions/ApplicationBuilderExtensions.cs index 09a50a3..0d5f0ae 100644 --- a/SPMS.API/Extensions/ApplicationBuilderExtensions.cs +++ b/SPMS.API/Extensions/ApplicationBuilderExtensions.cs @@ -56,11 +56,11 @@ public static class ApplicationBuilderExtensions // -- 11. 역할 인가 -- app.UseAuthorization(); - // -- 12. X-Service-Code 서비스 식별 (미구현 — Issue #13) -- - // app.UseMiddleware(); + // -- 12. X-Service-Code 서비스 식별 -- + app.UseMiddleware(); - // -- 13. X-API-KEY 검증 (미구현 — Issue #13) -- - // app.UseMiddleware(); + // -- 13. X-API-KEY 검증 (SDK/디바이스 엔드포인트용) -- + app.UseMiddleware(); // -- 14. X-SPMS-TEST 샌드박스 모드 (미구현 — Issue #14) -- // app.UseMiddleware(); diff --git a/SPMS.API/Middlewares/ApiKeyMiddleware.cs b/SPMS.API/Middlewares/ApiKeyMiddleware.cs new file mode 100644 index 0000000..e1a9143 --- /dev/null +++ b/SPMS.API/Middlewares/ApiKeyMiddleware.cs @@ -0,0 +1,49 @@ +using SPMS.Domain.Common; +using SPMS.Domain.Interfaces; + +namespace SPMS.API.Middlewares; + +public class ApiKeyMiddleware +{ + private readonly RequestDelegate _next; + + public ApiKeyMiddleware(RequestDelegate next) => _next = next; + + public async Task InvokeAsync(HttpContext context, IServiceRepository serviceRepository) + { + if (!RequiresApiKey(context.Request.Path)) + { + await _next(context); + return; + } + + if (!context.Request.Headers.TryGetValue("X-API-KEY", out var apiKey) || + string.IsNullOrWhiteSpace(apiKey)) + { + context.Response.StatusCode = 401; + context.Response.ContentType = "application/json"; + await context.Response.WriteAsJsonAsync( + ApiResponse.Fail(ErrorCodes.Unauthorized, "API Key가 필요합니다.")); + return; + } + + var service = await serviceRepository.GetByApiKeyAsync(apiKey!); + if (service == null) + { + context.Response.StatusCode = 403; + context.Response.ContentType = "application/json"; + await context.Response.WriteAsJsonAsync( + ApiResponse.Fail(ErrorCodes.Unauthorized, "유효하지 않은 API Key입니다.")); + return; + } + + context.Items["Service"] = service; + context.Items["ServiceId"] = service.Id; + await _next(context); + } + + private static bool RequiresApiKey(PathString path) + { + return path.StartsWithSegments("/v1/in/device"); + } +} diff --git a/SPMS.API/Middlewares/ServiceCodeMiddleware.cs b/SPMS.API/Middlewares/ServiceCodeMiddleware.cs new file mode 100644 index 0000000..241df27 --- /dev/null +++ b/SPMS.API/Middlewares/ServiceCodeMiddleware.cs @@ -0,0 +1,56 @@ +using SPMS.Domain.Common; +using SPMS.Domain.Enums; +using SPMS.Domain.Interfaces; + +namespace SPMS.API.Middlewares; + +public class ServiceCodeMiddleware +{ + private readonly RequestDelegate _next; + + public ServiceCodeMiddleware(RequestDelegate next) => _next = next; + + public async Task InvokeAsync(HttpContext context, IServiceRepository serviceRepository) + { + if (context.Request.Path.StartsWithSegments("/v1/out") || + context.Request.Path.StartsWithSegments("/swagger") || + context.Request.Path.StartsWithSegments("/health")) + { + await _next(context); + return; + } + + if (!context.Request.Headers.TryGetValue("X-Service-Code", out var serviceCode) || + string.IsNullOrWhiteSpace(serviceCode)) + { + context.Response.StatusCode = 400; + context.Response.ContentType = "application/json"; + await context.Response.WriteAsJsonAsync( + ApiResponse.Fail(ErrorCodes.BadRequest, "X-Service-Code 헤더가 필요합니다.")); + return; + } + + var service = await serviceRepository.GetByServiceCodeAsync(serviceCode!); + if (service == null) + { + context.Response.StatusCode = 404; + context.Response.ContentType = "application/json"; + await context.Response.WriteAsJsonAsync( + ApiResponse.Fail(ErrorCodes.NotFound, "존재하지 않는 서비스입니다.")); + return; + } + + if (service.Status != ServiceStatus.Active) + { + context.Response.StatusCode = 503; + context.Response.ContentType = "application/json"; + await context.Response.WriteAsJsonAsync( + ApiResponse.Fail(ErrorCodes.Unauthorized, "비활성 상태의 서비스입니다.")); + return; + } + + context.Items["Service"] = service; + context.Items["ServiceId"] = service.Id; + await _next(context); + } +} diff --git a/SPMS.Infrastructure/DependencyInjection.cs b/SPMS.Infrastructure/DependencyInjection.cs index 1fe5cb4..41bb640 100644 --- a/SPMS.Infrastructure/DependencyInjection.cs +++ b/SPMS.Infrastructure/DependencyInjection.cs @@ -24,6 +24,7 @@ public static class DependencyInjection // UnitOfWork & Repositories services.AddScoped(); services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); + services.AddScoped(); // External Services services.AddScoped(); diff --git a/SPMS.Infrastructure/Persistence/Repositories/ServiceRepository.cs b/SPMS.Infrastructure/Persistence/Repositories/ServiceRepository.cs new file mode 100644 index 0000000..d346ba9 --- /dev/null +++ b/SPMS.Infrastructure/Persistence/Repositories/ServiceRepository.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore; +using SPMS.Domain.Entities; +using SPMS.Domain.Enums; +using SPMS.Domain.Interfaces; + +namespace SPMS.Infrastructure.Persistence.Repositories; + +public class ServiceRepository : Repository, IServiceRepository +{ + public ServiceRepository(AppDbContext context) : base(context) { } + + public async Task GetByServiceCodeAsync(string serviceCode) + => await _dbSet.FirstOrDefaultAsync(s => s.ServiceCode == serviceCode); + + public async Task GetByApiKeyAsync(string apiKey) + => await _dbSet.FirstOrDefaultAsync(s => s.ApiKey == apiKey); + + public async Task GetByIdWithIpsAsync(long id) + => await _dbSet + .Include(s => s.ServiceIps) + .FirstOrDefaultAsync(s => s.Id == id); + + public async Task> GetByStatusAsync(ServiceStatus status) + => await _dbSet.Where(s => s.Status == status).ToListAsync(); +}