Handler.cs 3.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. using Application.Abstractions.Data;
  2. using Application.Abstractions.Messaging;
  3. using Application.Abstractions.Messaging.Email;
  4. using Domain.Entities.EmailVerification;
  5. using Domain.Entities.EmailVerification.ValueObject;
  6. using SharedKernel;
  7. using SharedKernel.Results;
  8. using Domain.Entities.Members.Logs;
  9. using Microsoft.EntityFrameworkCore;
  10. using Microsoft.Extensions.Options;
  11. using System.Security.Cryptography;
  12. namespace Application.Features.Api.MyPage.ChangeEmail;
  13. internal sealed class Handler(
  14. IAppDbContext db,
  15. IMailService mailService,
  16. IOptions<AppSettings> options
  17. ) : ICommandHandler<Command, Result> {
  18. public async Task<Result> Handle(Command request, CancellationToken ct)
  19. {
  20. var newEmail = request.NewEmail?.Trim().ToLower();
  21. if (string.IsNullOrWhiteSpace(newEmail))
  22. {
  23. return Result.Failure(Error.Problem("MyPage.EmailRequired", "이메일은 필수입니다."));
  24. }
  25. if (newEmail.Length > 60)
  26. {
  27. return Result.Failure(Error.Problem("MyPage.EmailTooLong", "이메일은 60자 이하여야 합니다."));
  28. }
  29. var member = await db.Member.FirstOrDefaultAsync(m => m.ID == request.MemberID, ct);
  30. if (member is null)
  31. {
  32. return Result.Failure(Error.NotFound("MyPage.MemberNotFound", "회원 정보를 찾을 수 없습니다."));
  33. }
  34. if (member.Email == newEmail)
  35. {
  36. return Result.Failure(Error.Problem("MyPage.SameEmail", "현재 이메일과 동일합니다."));
  37. }
  38. var emailExists = await db.Member.AnyAsync(m => m.Email == newEmail, ct);
  39. if (emailExists)
  40. {
  41. return Result.Failure(Error.Conflict("MyPage.EmailExists", "이미 사용 중인 이메일입니다."));
  42. }
  43. // 인증 토큰 생성
  44. var token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
  45. var expiration = DateTime.UtcNow.AddHours(24);
  46. var verifyToken = EmailVerifyToken.Create(
  47. VerificationType.ChangedEmail,
  48. newEmail,
  49. token,
  50. expiration,
  51. new AdditionalData { Email = member.Email }
  52. );
  53. await db.EmailVerifyToken.AddAsync(verifyToken, ct);
  54. // 변경 로그 기록
  55. var log = new MemberEmailChangeLog(
  56. request.MemberID,
  57. member.Email,
  58. newEmail,
  59. null,
  60. null,
  61. null
  62. );
  63. await db.MemberEmailChangeLog.AddAsync(log, ct);
  64. // 이메일 변경 처리
  65. member.SetEmail(newEmail);
  66. await db.SaveChangesAsync(ct);
  67. // 인증 메일 발송
  68. var frontURL = options.Value.App.FrontURL.TrimEnd('/');
  69. var verifyUrl = $"{frontURL}/auth/verify-email?token={Uri.EscapeDataString(token)}";
  70. await mailService.SendAsync(new SendData(
  71. newEmail,
  72. "[bitforum] 이메일 인증",
  73. $"<p>아래 링크를 클릭하여 이메일 인증을 완료해주세요.</p><p><a href=\"{verifyUrl}\">{verifyUrl}</a></p><p>이 링크는 24시간 동안 유효합니다.</p>"
  74. ), ct);
  75. return Result.Success();
  76. }
  77. }