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}"));
}
}