Handler.cs 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. using Application.Abstractions.Authentication;
  2. using Application.Abstractions.Crypto;
  3. using Application.Abstractions.Data;
  4. using Application.Abstractions.Messaging;
  5. using Application.Helpers;
  6. using SharedKernel;
  7. using SharedKernel.Results;
  8. using Microsoft.EntityFrameworkCore;
  9. using Microsoft.Extensions.Options;
  10. namespace Application.Features.Api.Auth.SocialAuthorizeUrl;
  11. internal sealed class Handler(
  12. IEnumerable<ISocialAuthProvider> socialProviders,
  13. IAppDbContext db,
  14. IFieldEncryptor fieldEncryptor,
  15. IOptions<AppSettings> options
  16. ) : IQueryHandler<Query, Result<Response>> {
  17. private readonly AppSettings.OAuthSection _oauth = options.Value.OAuth;
  18. public async Task<Result<Response>> Handle(Query request, CancellationToken ct)
  19. {
  20. var providerName = request.Provider?.Trim().ToLowerInvariant() switch
  21. {
  22. "naver" => "Naver",
  23. "kakao" => "Kakao",
  24. _ => null
  25. };
  26. if (providerName is null)
  27. {
  28. return Result.Failure<Response>(Error.Problem("Auth.UnsupportedProvider", "지원하지 않는 소셜 로그인입니다."));
  29. }
  30. // 네이버는 스펙상 필수, 카카오도 CSRF 방어 위해 항상 요구 (d5 §③)
  31. if (string.IsNullOrWhiteSpace(request.State))
  32. {
  33. return Result.Failure<Response>(Error.Problem("Auth.StateRequired", "state 값이 필요합니다."));
  34. }
  35. var oauthConfig = providerName == "Naver" ? _oauth.Naver : _oauth.Kakao;
  36. // 키·활성화는 DB Config(External, Admin 관리) 우선 — 없으면 appsettings 폴백. RedirectUri 는 환경별 appsettings 유지.
  37. // (SocialLogin/Handler 와 동일 패턴 — 이전엔 여기만 appsettings 만 봐서 DB 키가 안 잡혔음)
  38. var ext = (await db.Config.AsNoTracking().OrderByDescending(c => c.ID).FirstOrDefaultAsync(ct))?.External;
  39. string Reveal(string? enc) => string.IsNullOrWhiteSpace(enc) ? "" : EncryptedConfigValue.Unprotect(fieldEncryptor, enc);
  40. // 관리자 on/off (Admin /Config/External 체크박스). Config 행이 없으면 도메인 기본(true) 취급.
  41. var isEnabled = providerName == "Naver" ? (ext?.NaverLoginEnabled ?? true) : (ext?.KakaoLoginEnabled ?? true);
  42. if (!isEnabled)
  43. {
  44. return Result.Failure<Response>(Error.Problem("Auth.SocialDisabled", $"{providerName} 로그인을 사용할 수 없습니다."));
  45. }
  46. var effectiveClientId = providerName == "Naver"
  47. ? (!string.IsNullOrWhiteSpace(ext?.NaverClientId) ? ext!.NaverClientId! : oauthConfig.ClientId)
  48. : (!string.IsNullOrWhiteSpace(ext?.KakaoRestApiKeyEnc) ? Reveal(ext!.KakaoRestApiKeyEnc) : oauthConfig.ClientId);
  49. if (string.IsNullOrWhiteSpace(effectiveClientId))
  50. {
  51. return Result.Failure<Response>(Error.Problem("Auth.SocialNotConfigured", $"{providerName} 로그인이 설정되지 않았습니다."));
  52. }
  53. var redirectUri = string.IsNullOrWhiteSpace(request.RedirectUri) ? oauthConfig.RedirectUri : request.RedirectUri;
  54. if (string.IsNullOrWhiteSpace(redirectUri))
  55. {
  56. return Result.Failure<Response>(Error.Problem("Auth.RedirectUriRequired", "Redirect URI 가 필요합니다."));
  57. }
  58. var provider = socialProviders.FirstOrDefault(c => c.Provider == providerName);
  59. if (provider is null)
  60. {
  61. return Result.Failure<Response>(Error.Problem("Auth.UnsupportedProvider", "지원하지 않는 소셜 로그인입니다."));
  62. }
  63. var url = provider.GetAuthorizationUrl(effectiveClientId, redirectUri, request.State);
  64. return Result.Success(new Response(providerName, url));
  65. }
  66. }