| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106 |
- using Application.Abstractions.Data;
- using Application.Abstractions.Cache;
- using Application.Abstractions.Messaging;
- using Application.Abstractions.Messaging.Email;
- using Application.Helpers;
- using Domain.Entities.EmailVerification;
- using Domain.Entities.EmailVerification.ValueObject;
- using SharedKernel;
- using SharedKernel.Results;
- using Domain.Entities.Members.Logs;
- using Microsoft.EntityFrameworkCore;
- using Microsoft.Extensions.Options;
- using System.Security.Cryptography;
- namespace Application.Features.Api.MyPage.ChangeEmail;
- internal sealed class Handler(
- IAppDbContext db,
- ICacheService cache,
- IMailService mailService,
- IOptions<AppSettings> options
- ) : ICommandHandler<Command, Result> {
- public async Task<Result> Handle(Command request, CancellationToken ct)
- {
- var newEmail = request.NewEmail?.Trim().ToLower();
- if (string.IsNullOrWhiteSpace(newEmail))
- {
- return Result.Failure(Error.Problem("MyPage.EmailRequired", "이메일은 필수입니다."));
- }
- if (newEmail.Length > 60)
- {
- return Result.Failure(Error.Problem("MyPage.EmailTooLong", "이메일은 60자 이하여야 합니다."));
- }
- var member = await db.Member.FirstOrDefaultAsync(m => m.ID == request.MemberID, ct);
- if (member is null)
- {
- return Result.Failure(Error.NotFound("MyPage.MemberNotFound", "회원 정보를 찾을 수 없습니다."));
- }
- // 이메일 변경 주기 확인
- var accountConfig = await AccountConfigLoader.GetAccountConfigAsync(cache, db, ct);
- if (accountConfig.ChangeEmailDay is > 0 && member.LastEmailChangedAt.HasValue)
- {
- var nextChangeDate = member.LastEmailChangedAt.Value.AddDays(accountConfig.ChangeEmailDay.Value);
- if (DateTime.UtcNow < nextChangeDate)
- {
- return Result.Failure(Error.Problem("MyPage.EmailChangeTooSoon",
- $"이메일은 {accountConfig.ChangeEmailDay}일마다 변경 가능합니다."));
- }
- }
- if (member.Email == newEmail)
- {
- return Result.Failure(Error.Problem("MyPage.SameEmail", "현재 이메일과 동일합니다."));
- }
- var emailExists = await db.Member.AnyAsync(m => m.Email == newEmail, ct);
- if (emailExists)
- {
- return Result.Failure(Error.Conflict("MyPage.EmailExists", "이미 사용 중인 이메일입니다."));
- }
- // 인증 토큰 생성
- var token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32));
- var expiration = DateTime.UtcNow.AddHours(24);
- var verifyToken = EmailVerifyToken.Create(
- VerificationType.ChangedEmail,
- newEmail,
- token,
- expiration,
- new AdditionalData { Email = member.Email }
- );
- await db.EmailVerifyToken.AddAsync(verifyToken, ct);
- // 변경 로그 기록
- var log = new MemberEmailChangeLog(
- request.MemberID,
- member.Email,
- newEmail,
- request.Referrer,
- request.IpAddress,
- request.UserAgent
- );
- await db.MemberEmailChangeLog.AddAsync(log, ct);
- await db.SaveChangesAsync(ct);
- // 인증 메일 발송
- var frontURL = options.Value.App.FrontURL.TrimEnd('/');
- var verifyUrl = $"{frontURL}/verify-email?token={Uri.EscapeDataString(token)}";
- await mailService.SendAsync(new SendData(
- newEmail,
- "[bitforum] 이메일 인증",
- $"<p>아래 링크를 클릭하여 이메일 인증을 완료해주세요.</p><p><a href=\"{verifyUrl}\">{verifyUrl}</a></p><p>이 링크는 24시간 동안 유효합니다.</p>"
- ), ct);
- return Result.Success();
- }
- }
|