| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100 |
- using Application.Abstractions.Messaging;
- using Web.Api.Common;
- namespace Web.Api.Endpoints.Auth;
- /// <summary>
- /// 소셜 "연결 끊기(disconnect/unlink)" webhook — 카카오/네이버 (익명, GET·POST 둘 다 허용).
- /// 카카오는 3초 내 2xx 를 요구하므로 검증 통과 시 연동행 삭제 후 항상 200 반환(멱등).
- /// 카카오 Admin 키 불일치만 401 로 거부. 수신 파라미터 전체를 정보 로그로 남겨 실제 규격을 확정한다.
- /// </summary>
- internal sealed class SocialDisconnect : IEndpoint
- {
- public void MapEndpoint(IEndpointRouteBuilder app)
- {
- app.MapMethods("api/auth/kakao/disconnect", ["GET", "POST"], (
- HttpContext httpContext,
- ISender sender,
- ILogger<SocialDisconnect> 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<SocialDisconnect> logger,
- CancellationToken ct
- ) => HandleAsync("Naver", "id", httpContext, sender, logger, ct))
- .WithTags("Auth")
- .AllowAnonymous();
- }
- private static async Task<IResult> HandleAsync(
- string provider,
- string userKeyParam,
- HttpContext httpContext,
- ISender sender,
- ILogger<SocialDisconnect> 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);
- }
- /// <summary>쿼리스트링(GET)·form-urlencoded(POST) 양쪽에서 파라미터를 수집. form 값이 우선.</summary>
- private static async Task<Dictionary<string, string>> CollectParametersAsync(HttpContext httpContext, CancellationToken ct)
- {
- var parameters = new Dictionary<string, string>(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<string, string> parameters)
- {
- return parameters.Count == 0 ? "(none)" : string.Join(", ", parameters.Select(c => $"{c.Key}={c.Value}"));
- }
- }
|