Handler.cs 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. using System.Security.Cryptography;
  2. using Application.Abstractions.Data;
  3. using Application.Abstractions.Kyc;
  4. using Application.Abstractions.Messaging;
  5. using Domain.Entities.Developers;
  6. using Domain.Entities.Developers.ValueObject;
  7. using Microsoft.EntityFrameworkCore;
  8. using SharedKernel.Results;
  9. namespace Application.Features.Api.Developers.Profile.StartKyc;
  10. internal sealed class Handler(IAppDbContext db, INiceAuthClient nice) : ICommandHandler<Command, Result<Response>>
  11. {
  12. public async Task<Result<Response>> Handle(Command request, CancellationToken ct)
  13. {
  14. var dev = await db.Developer
  15. .AsTracking()
  16. .FirstOrDefaultAsync(c => c.MemberID == request.MemberID, ct);
  17. if (dev is null)
  18. {
  19. return Result.Failure<Response>(Error.NotFound(
  20. "Developer.NotFound",
  21. "회사 정보를 먼저 입력해 주세요."));
  22. }
  23. if (dev.KycStatus == KycStatus.Verified)
  24. {
  25. return Result.Failure<Response>(Error.Conflict(
  26. "Developer.AlreadyVerified",
  27. "이미 본인인증이 완료되었어요."));
  28. }
  29. // 기존 Pending 중 만료된 세션은 Expired 로 정리 (DB 위생)
  30. var now = DateTime.UtcNow;
  31. var stale = await db.NiceAuthSession
  32. .AsTracking()
  33. .Where(c => c.DeveloperID == dev.ID && c.Status == NiceAuthSessionStatus.Pending && c.ExpiresAt <= now)
  34. .ToListAsync(ct);
  35. foreach (var s in stale)
  36. {
  37. s.MarkExpired();
  38. }
  39. var sessionToken = GenerateSessionToken();
  40. var urlResult = await nice.RequestAuthUrlAsync(sessionToken, ct);
  41. if (urlResult.IsFailure)
  42. {
  43. return Result.Failure<Response>(urlResult.Error);
  44. }
  45. var info = urlResult.Value;
  46. var session = NiceAuthSession.Create(
  47. dev.ID,
  48. sessionToken,
  49. info.RequestNo,
  50. info.TransactionId,
  51. info.Ticket,
  52. info.Iterators);
  53. await db.NiceAuthSession.AddAsync(session, ct);
  54. dev.MarkKycPending();
  55. await db.SaveChangesAsync(ct);
  56. return Result.Success(new Response(info.AuthUrl, sessionToken));
  57. }
  58. /// <summary>
  59. /// SessionToken: 16 byte CSPRNG → URL-safe Base64 no-padding (22 char).
  60. /// NICE return_url path segment 로 embed 되므로 URL-safe 문자만 사용.
  61. /// </summary>
  62. private static string GenerateSessionToken()
  63. {
  64. var bytes = RandomNumberGenerator.GetBytes(16);
  65. return Convert.ToBase64String(bytes)
  66. .Replace('+', '-')
  67. .Replace('/', '_')
  68. .TrimEnd('=');
  69. }
  70. }