Handler.cs 3.8 KB

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