using System.Security.Cryptography; using Application.Abstractions.Data; using Application.Abstractions.Kyc; using Application.Abstractions.Messaging; using Domain.Entities.Developers; using Domain.Entities.Developers.ValueObject; using Microsoft.EntityFrameworkCore; using SharedKernel.Results; namespace Application.Features.Api.Developers.Profile.StartKyc; internal sealed class Handler(IAppDbContext db, INiceAuthClient nice) : ICommandHandler> { public async Task> Handle(Command request, CancellationToken ct) { var dev = await db.Developer .AsTracking() .FirstOrDefaultAsync(c => c.MemberID == request.MemberID, ct); if (dev is null) { return Result.Failure(Error.NotFound( "Developer.NotFound", "회사 정보를 먼저 입력해 주세요.")); } if (dev.KycStatus == KycStatus.Verified) { return Result.Failure(Error.Conflict( "Developer.AlreadyVerified", "이미 본인인증이 완료되었어요.")); } // 기존 Pending 중 만료된 세션은 Expired 로 정리 (DB 위생) var now = DateTime.UtcNow; var stale = await db.NiceAuthSession .AsTracking() .Where(c => c.DeveloperID == dev.ID && c.Status == NiceAuthSessionStatus.Pending && c.ExpiresAt <= now) .ToListAsync(ct); foreach (var s in stale) { s.MarkExpired(); } var sessionToken = GenerateSessionToken(); var urlResult = await nice.RequestAuthUrlAsync(sessionToken, ct); if (urlResult.IsFailure) { return Result.Failure(urlResult.Error); } var info = urlResult.Value; var session = NiceAuthSession.Create( dev.ID, sessionToken, info.RequestNo, info.TransactionId, info.Ticket, info.Iterators); await db.NiceAuthSession.AddAsync(session, ct); dev.MarkKycPending(); await db.SaveChangesAsync(ct); return Result.Success(new Response(info.AuthUrl, sessionToken)); } /// /// SessionToken: 16 byte CSPRNG → URL-safe Base64 no-padding (22 char). /// NICE return_url path segment 로 embed 되므로 URL-safe 문자만 사용. /// private static string GenerateSessionToken() { var bytes = RandomNumberGenerator.GetBytes(16); return Convert.ToBase64String(bytes) .Replace('+', '-') .Replace('/', '_') .TrimEnd('='); } }