using Application.Abstractions.Messaging; using Web.Api.Common; namespace Web.Api.Endpoints.Auth; /// /// 소셜 "연결 끊기(disconnect/unlink)" webhook — 카카오/네이버 (익명, GET·POST 둘 다 허용). /// 카카오는 3초 내 2xx 를 요구하므로 검증 통과 시 연동행 삭제 후 항상 200 반환(멱등). /// 카카오 Admin 키 불일치만 401 로 거부. 수신 파라미터 전체를 정보 로그로 남겨 실제 규격을 확정한다. /// internal sealed class SocialDisconnect : IEndpoint { public void MapEndpoint(IEndpointRouteBuilder app) { app.MapMethods("api/auth/kakao/disconnect", ["GET", "POST"], ( HttpContext httpContext, ISender sender, ILogger logger, CancellationToken ct ) => HandleAsync("Kakao", "user_id", httpContext, sender, logger, ct)) .WithTags("Auth") .AllowAnonymous(); app.MapMethods("api/auth/naver/disconnect", ["GET", "POST"], ( HttpContext httpContext, ISender sender, ILogger logger, CancellationToken ct ) => HandleAsync("Naver", "id", httpContext, sender, logger, ct)) .WithTags("Auth") .AllowAnonymous(); } private static async Task HandleAsync( string provider, string userKeyParam, HttpContext httpContext, ISender sender, ILogger logger, CancellationToken ct ) { var parameters = await CollectParametersAsync(httpContext, ct); // 규격 확정용: 수신 파라미터 전체 + Authorization 존재 여부를 정보 로그로 남긴다 (헤더 원문/토큰은 남기지 않음). logger.LogInformation( "{Provider} disconnect 수신 — method={Method}, hasAuthorization={HasAuth}, params={Params}", provider, httpContext.Request.Method, httpContext.Request.Headers.ContainsKey("Authorization"), FormatParameters(parameters) ); var providerUserKey = parameters.TryGetValue(userKeyParam, out var key) ? key : null; var adminKeyHeader = httpContext.Request.Headers.Authorization.ToString(); var command = new Application.Features.Api.Auth.SocialDisconnect.Command( provider, providerUserKey, string.IsNullOrEmpty(adminKeyHeader) ? null : adminKeyHeader ); var result = await sender.Send(command, ct); // 성공/멱등은 항상 200. Admin 키 불일치 등 검증 실패만 해당 상태코드로 거부. if (result.IsSuccess) { return Results.Ok(new { provider = result.Value.Provider, removed = result.Value.Removed }); } return CustomResults.Problem(result); } /// 쿼리스트링(GET)·form-urlencoded(POST) 양쪽에서 파라미터를 수집. form 값이 우선. private static async Task> CollectParametersAsync(HttpContext httpContext, CancellationToken ct) { var parameters = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var kv in httpContext.Request.Query) { parameters[kv.Key] = kv.Value.ToString(); } if (httpContext.Request.HasFormContentType) { var form = await httpContext.Request.ReadFormAsync(ct); foreach (var kv in form) { parameters[kv.Key] = kv.Value.ToString(); } } return parameters; } private static string FormatParameters(Dictionary parameters) { return parameters.Count == 0 ? "(none)" : string.Join(", ", parameters.Select(c => $"{c.Key}={c.Value}")); } }