using Application.Abstractions.Authentication; using Application.Abstractions.Crypto; using Application.Abstractions.Data; using Application.Abstractions.Messaging; using Application.Helpers; using SharedKernel; using SharedKernel.Results; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; namespace Application.Features.Api.Auth.SocialAuthorizeUrl; internal sealed class Handler( IEnumerable socialProviders, IAppDbContext db, IFieldEncryptor fieldEncryptor, IOptions options ) : IQueryHandler> { private readonly AppSettings.OAuthSection _oauth = options.Value.OAuth; public async Task> Handle(Query request, CancellationToken ct) { var providerName = request.Provider?.Trim().ToLowerInvariant() switch { "naver" => "Naver", "kakao" => "Kakao", _ => null }; if (providerName is null) { return Result.Failure(Error.Problem("Auth.UnsupportedProvider", "지원하지 않는 소셜 로그인입니다.")); } // 네이버는 스펙상 필수, 카카오도 CSRF 방어 위해 항상 요구 (d5 §③) if (string.IsNullOrWhiteSpace(request.State)) { return Result.Failure(Error.Problem("Auth.StateRequired", "state 값이 필요합니다.")); } var oauthConfig = providerName == "Naver" ? _oauth.Naver : _oauth.Kakao; // 키·활성화는 DB Config(External, Admin 관리) 우선 — 없으면 appsettings 폴백. RedirectUri 는 환경별 appsettings 유지. // (SocialLogin/Handler 와 동일 패턴 — 이전엔 여기만 appsettings 만 봐서 DB 키가 안 잡혔음) var ext = (await db.Config.AsNoTracking().OrderByDescending(c => c.ID).FirstOrDefaultAsync(ct))?.External; string Reveal(string? enc) => string.IsNullOrWhiteSpace(enc) ? "" : EncryptedConfigValue.Unprotect(fieldEncryptor, enc); // 관리자 on/off (Admin /Config/External 체크박스). Config 행이 없으면 도메인 기본(true) 취급. var isEnabled = providerName == "Naver" ? (ext?.NaverLoginEnabled ?? true) : (ext?.KakaoLoginEnabled ?? true); if (!isEnabled) { return Result.Failure(Error.Problem("Auth.SocialDisabled", $"{providerName} 로그인을 사용할 수 없습니다.")); } var effectiveClientId = providerName == "Naver" ? (!string.IsNullOrWhiteSpace(ext?.NaverClientId) ? ext!.NaverClientId! : oauthConfig.ClientId) : (!string.IsNullOrWhiteSpace(ext?.KakaoRestApiKeyEnc) ? Reveal(ext!.KakaoRestApiKeyEnc) : oauthConfig.ClientId); if (string.IsNullOrWhiteSpace(effectiveClientId)) { return Result.Failure(Error.Problem("Auth.SocialNotConfigured", $"{providerName} 로그인이 설정되지 않았습니다.")); } var redirectUri = string.IsNullOrWhiteSpace(request.RedirectUri) ? oauthConfig.RedirectUri : request.RedirectUri; if (string.IsNullOrWhiteSpace(redirectUri)) { return Result.Failure(Error.Problem("Auth.RedirectUriRequired", "Redirect URI 가 필요합니다.")); } var provider = socialProviders.FirstOrDefault(c => c.Provider == providerName); if (provider is null) { return Result.Failure(Error.Problem("Auth.UnsupportedProvider", "지원하지 않는 소셜 로그인입니다.")); } var url = provider.GetAuthorizationUrl(effectiveClientId, redirectUri, request.State); return Result.Success(new Response(providerName, url)); } }