| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323 |
- using System.ComponentModel.DataAnnotations;
- using System.ComponentModel.DataAnnotations.Schema;
- using System.Security.Cryptography;
- using Domain.Entities.Members.ValueObject;
- using Domain.Entities.Wallets;
- using NanoidDotNet;
- /// <summary>
- /// 사용자단 일반 회원 정보
- /// </summary>
- namespace Domain.Entities.Members;
- public class Member
- {
- [Key]
- public int ID { get; private set; }
- // 회원 등급 정보
- [ForeignKey(nameof(MemberGradeID))]
- public virtual MemberGrade? MemberGrade { get; private set; }
- // 알림/약관 동의 여부
- public virtual MemberApprove MemberApprove { get; private set; } = null!;
- // 회원 집계 정보
- public virtual MemberStats MemberStats { get; private set; } = null!;
- // 지갑
- public virtual Wallet? Wallet { get; private set; }
- // 채널
- public virtual Channel? Channel { get; private set; }
- public int? MemberGradeID { get; private set; }
- public string SID { get; private set; } = Nanoid.Generate(size: 8); // 무작위 문자열
- public string Email { get; private set; } = default!;
- public string? Name { get; private set; }
- public string? FullName { get; private set; }
- public string? FirstName { get; private set; }
- public string? LastName { get; private set; }
- public string? Password { get; private set; } = string.Empty; // 관리자 수정 시 필요
- public string? PasswordHash { get; private set; }
- public string? Intro { get; private set; }
- public string? Summary { get; private set; }
- public string? Phone { get; private set; }
- public DateOnly? Birthday { get; private set; }
- public Gender? Gender { get; private set; }
- public string? Thumb { get; private set; }
- public string? BannerUrl { get; private set; }
- public string? Icon { get; private set; }
- /// <summary>
- /// PG customerKey (토스페이먼츠 — 예측 가능 값 금지, 회원별 UUID).
- /// 결제 주문 생성 시 없으면 발급·저장한다. 마이그레이션은 후속 처리.
- /// </summary>
- public Guid? PaymentCustomerKey { get; private set; }
- public bool IsEmailVerified { get; private set; } = false;
- public bool IsAuthCertified { get; private set; } = false;
- public bool IsDenied { get; private set; } = false;
- public bool IsAdmin { get; private set; } = false;
- public bool IsWithdraw { get; private set; } = false;
- public bool IsCreator { get; private set; } = false;
- public string? DeviceInfo { get; private set; }
- public string? SignupIP { get; private set; }
- public string? LastLoginIp { get; private set; }
- public string? IpAddress { get; private set; }
- public string? UserAgent { get; private set; }
- public DateTime? LastLoginAt { get; private set; }
- public DateTime? LastEmailChangedAt { get; private set; }
- public DateTime? LastNameChangedAt { get; private set; }
- public DateTime? LastSummaryChangedAt { get; private set; }
- public DateTime? LastIntroChangedAt { get; private set; }
- public DateTime? LastThumbChangedAt { get; private set; }
- public DateTime? EmailVerifiedAt { get; private set; }
- public DateTime? AuthCertifiedAt { get; private set; }
- public DateTime PasswordUpdatedAt { get; private set; }
- public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;
- public DateTime? UpdatedAt { get; private set; }
- public DateTime? DeletedAt { get; private set; }
- public DateTime? DeniedAt { get; private set; }
- private Member() { }
- private Member(string email)
- {
- if (string.IsNullOrWhiteSpace(email))
- {
- throw new ArgumentException("Email is required.", nameof(email));
- }
- if (email.Length > 60)
- {
- throw new ArgumentOutOfRangeException(nameof(email));
- }
- Email = email;
- PasswordUpdatedAt = DateTime.UtcNow;
- }
- public static Member Create(string email)
- {
- return new(email);
- }
- public static Member Create(string email, string password)
- {
- var member = new Member(email);
- member.SetPassword(password);
- return member;
- }
- public void SetPassword(string password)
- {
- PasswordHash = HashPassword(password);
- PasswordUpdatedAt = DateTime.UtcNow;
- }
- public bool VerifyPassword(string password)
- {
- if (string.IsNullOrEmpty(PasswordHash))
- {
- return false;
- }
- return VerifyHashedPassword(PasswordHash, password);
- }
- public void SetEmail(string email)
- {
- Email = email;
- IsEmailVerified = false;
- LastEmailChangedAt = DateTime.UtcNow;
- UpdatedAt = DateTime.UtcNow;
- }
- public void SetName(string? name)
- {
- Name = name;
- LastNameChangedAt = DateTime.UtcNow;
- UpdatedAt = DateTime.UtcNow;
- }
- public void SetSummary(string? summary)
- {
- Summary = summary;
- LastSummaryChangedAt = DateTime.UtcNow;
- UpdatedAt = DateTime.UtcNow;
- }
- public void SetIntro(string? intro)
- {
- Intro = intro;
- LastIntroChangedAt = DateTime.UtcNow;
- UpdatedAt = DateTime.UtcNow;
- }
- public void SetThumb(string? thumb)
- {
- Thumb = thumb;
- LastThumbChangedAt = DateTime.UtcNow;
- UpdatedAt = DateTime.UtcNow;
- }
- public void SetBannerUrl(string? bannerUrl)
- {
- BannerUrl = bannerUrl;
- UpdatedAt = DateTime.UtcNow;
- }
- public void SetCreator(bool isCreator)
- {
- IsCreator = isCreator;
- UpdatedAt = DateTime.UtcNow;
- }
- /// <summary>회원 등급 설정 — 보상 엔진 자동 승급(래칫)에서 사용 (d3 M2).</summary>
- public void SetMemberGrade(int memberGradeID)
- {
- MemberGradeID = memberGradeID;
- UpdatedAt = DateTime.UtcNow;
- }
- /// <summary>PG customerKey 확보 — 없으면 이 시점에 UUID 발급 (호출측이 SaveChanges 로 영속화)</summary>
- public Guid EnsurePaymentCustomerKey()
- {
- if (PaymentCustomerKey is null)
- {
- PaymentCustomerKey = Guid.NewGuid();
- UpdatedAt = DateTime.UtcNow;
- }
- return PaymentCustomerKey.Value;
- }
- public void MarkEmailVerified()
- {
- IsEmailVerified = true;
- EmailVerifiedAt = DateTime.UtcNow;
- UpdatedAt = DateTime.UtcNow;
- }
- public void Withdraw()
- {
- IsWithdraw = true;
- DeletedAt = DateTime.UtcNow;
- UpdatedAt = DateTime.UtcNow;
- }
- public void RecordLogin(string? ipAddress, string? userAgent = null)
- {
- // DB 컬럼 길이 방어 — Cloudflare 더블 경유 등으로 비정상 길이 값이 와도 throw 대신 truncate.
- // (IpAddress/LastLoginIp nvarchar(45), UserAgent nvarchar(255))
- if (ipAddress is not null && ipAddress.Length > 45)
- {
- ipAddress = ipAddress[..45];
- }
- if (userAgent is not null && userAgent.Length > 255)
- {
- userAgent = userAgent[..255];
- }
- LastLoginAt = DateTime.UtcNow;
- LastLoginIp = ipAddress;
- if (!string.IsNullOrWhiteSpace(ipAddress))
- {
- IpAddress = ipAddress;
- }
- if (!string.IsNullOrWhiteSpace(userAgent))
- {
- UserAgent = userAgent;
- }
- UpdatedAt = DateTime.UtcNow;
- }
- // PBKDF2 (RFC 2898) - 순수 C# 비밀번호 해싱
- // public: Admin 회원 생성/수정의 bulk ExecuteUpdate 에서 평문 대신 해시를 저장하기 위해 노출
- public static string HashPassword(string password)
- {
- var salt = RandomNumberGenerator.GetBytes(16);
- var hash = Rfc2898DeriveBytes.Pbkdf2(
- password,
- salt,
- iterations: 100_000,
- HashAlgorithmName.SHA256,
- outputLength: 32
- );
- // salt + hash → Base64
- var combined = new byte[salt.Length + hash.Length];
- Buffer.BlockCopy(salt, 0, combined, 0, salt.Length);
- Buffer.BlockCopy(hash, 0, combined, salt.Length, hash.Length);
- return Convert.ToBase64String(combined);
- }
- // 비밀번호 검증
- private static bool VerifyHashedPassword(string hashedPassword, string password)
- {
- var combined = Convert.FromBase64String(hashedPassword);
- if (combined.Length != 48) // 16(salt) + 32(hash)
- {
- return false;
- }
- var salt = combined[..16];
- var storedHash = combined[16..];
- var hash = Rfc2898DeriveBytes.Pbkdf2(
- password,
- salt,
- iterations: 100_000,
- HashAlgorithmName.SHA256,
- outputLength: 32
- );
- return CryptographicOperations.FixedTimeEquals(storedHash, hash);
- }
- }
|