| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138 |
- using Application.Abstractions.Authentication;
- using Application.Abstractions.Cache;
- using Application.Abstractions.Data;
- using Application.Helpers;
- using Domain.Entities.Members.Logs;
- using MediatR;
- using SharedKernel;
- using SharedKernel.Results;
- using Microsoft.EntityFrameworkCore;
- using Microsoft.Extensions.Logging;
- using Microsoft.Extensions.Options;
- namespace Application.Features.Api.Auth.Login;
- internal sealed class Handler(
- IAppDbContext db,
- IJwtTokenProvider jwtTokenProvider,
- IOptions<AppSettings> options,
- ILogger<Handler> logger,
- ICacheService cache
- ) : IRequestHandler<Command, Result<Response>> {
- private readonly AppSettings.JwtSection _jwt = options.Value.JWT;
- public async Task<Result<Response>> Handle(Command request, CancellationToken ct)
- {
- // 유효성 검사
- if (string.IsNullOrWhiteSpace(request.Email))
- {
- return Result.Failure<Response>(Error.Problem("Auth.EmailRequired", "이메일은 필수입니다."));
- }
- if (string.IsNullOrWhiteSpace(request.Password))
- {
- return Result.Failure<Response>(Error.Problem("Auth.PasswordRequired", "비밀번호는 필수입니다."));
- }
- // Config 로드
- var accountConfig = await AccountConfigLoader.GetAccountConfigAsync(cache, db, ct);
- // Member 조회 (비밀번호 검증을 위해 Tracking 모드)
- var email = request.Email.Trim().ToLower();
- var member = await db.Member.FirstOrDefaultAsync(m => m.Email == email, ct);
- if (member is null)
- {
- // 실패 로그 기록 (회원 없음)
- await LogLoginAttempt(null, false, email, "회원 정보 없음", request, ct);
- return Result.Failure<Response>(Error.Unauthorized("Auth.InvalidCredentials", "이메일 또는 비밀번호가 올바르지 않습니다."));
- }
- // 탈퇴 회원 거부
- if (member.IsWithdraw)
- {
- await LogLoginAttempt(member.ID, false, email, "탈퇴 회원", request, ct);
- return Result.Failure<Response>(Error.Problem("Auth.MemberWithdrawn", "탈퇴한 회원은 이용할 수 없습니다."));
- }
- // 차단 회원 거부
- if (member.IsDenied)
- {
- await LogLoginAttempt(member.ID, false, email, "차단 회원", request, ct);
- return Result.Failure<Response>(Error.Problem("Auth.MemberDenied", "차단된 회원이므로 이용할 수 없습니다."));
- }
- // 로그인 시도 횟수 제한 확인
- if (accountConfig.MaxLoginTryCount is > 0 && accountConfig.MaxLoginTryLimitSecond is > 0)
- {
- var limitTime = DateTime.UtcNow.AddSeconds(-accountConfig.MaxLoginTryLimitSecond.Value);
- var failedCount = await db.MemberLoginLog.CountAsync(l => l.MemberID == member.ID && !l.Success && l.CreatedAt >= limitTime, ct);
- if (failedCount >= accountConfig.MaxLoginTryCount.Value)
- {
- await LogLoginAttempt(member.ID, false, email, "로그인 시도 횟수 초과", request, ct);
- return Result.Failure<Response>(Error.Problem("Auth.LoginTryExceeded", $"로그인 시도 횟수를 초과하였습니다. {accountConfig.MaxLoginTryLimitSecond}초 후에 다시 시도해주세요."));
- }
- }
- // 비밀번호 검증
- if (!member.VerifyPassword(request.Password))
- {
- await LogLoginAttempt(member.ID, false, email, "비밀번호 불일치", request, ct);
- return Result.Failure<Response>(Error.Unauthorized("Auth.InvalidCredentials", "이메일 또는 비밀번호가 올바르지 않습니다."));
- }
- // 로그인 시 이메일 인증자 여부 확인
- if (accountConfig.IsLoginEmailVerifiedOnly && !member.IsEmailVerified)
- {
- await LogLoginAttempt(member.ID, false, email, "이메일 미인증", request, ct);
- return Result.Failure<Response>(Error.Unauthorized("Auth.EmailNotVerified", "이메일 인증이 완료되지 않은 사용자는 로그인할 수 없습니다."));
- }
- // JWT 토큰 생성
- var accessToken = jwtTokenProvider.CreateAccessToken(member.ID, member.Email, member.Name);
- var refreshToken = jwtTokenProvider.CreateRefreshToken();
- var expiresAt = DateTime.UtcNow.AddMinutes(_jwt.AccessTokenExpiration);
- // RefreshToken 저장
- var refreshTokenEntity = Domain.Entities.Members.RefreshToken.Create(
- member.ID,
- refreshToken,
- DateTime.UtcNow.AddDays(_jwt.RefreshTokenExpiration)
- );
- await db.RefreshToken.AddAsync(refreshTokenEntity, ct);
- // 로그인 횟수 증가
- var memberStats = await db.MemberStats.FirstOrDefaultAsync(x => x.MemberID == member.ID, ct);
- if (memberStats is not null)
- {
- memberStats.LoginCount++;
- }
- // 성공 로그 기록
- await LogLoginAttempt(member.ID, true, email, null, request, ct);
- await db.SaveChangesAsync(ct);
- logger.LogInformation("{0} 로그인", member.Email);
- return Result.Success(new Response(accessToken, refreshToken, expiresAt));
- }
- private async Task LogLoginAttempt(int? memberID, bool success, string account, string? reason, Command request, CancellationToken ct)
- {
- var log = MemberLoginLog.Create(
- memberID,
- success,
- account,
- reason,
- ipAddress: request.IpAddress,
- userAgent: request.UserAgent
- );
- await db.MemberLoginLog.AddAsync(log, ct);
- await db.SaveChangesAsync(ct);
- }
- }
|