using System; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using System.Threading.Tasks; using D3ROVoice.Api.Data; using D3ROVoice.Api.Dtos; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.AspNetCore.Identity; using Microsoft.IdentityModel.Tokens; namespace D3ROVoice.Api.Services; public interface IAuthService { Task RegisterAsync(RegisterDto dto); Task LoginAsync(LoginDto dto); Task GetUserByEmailAsync(string email); } public class AuthService : IAuthService { private static readonly SemaphoreSlim BootstrapLock = new(1, 1); private readonly AppDbContext _db; private readonly IConfiguration _config; private readonly PasswordHasher _passwordHasher = new(); private readonly string _dummyPasswordHash; public AuthService(AppDbContext db, IConfiguration config) { _db = db; _config = config; _dummyPasswordHash = _passwordHasher.HashPassword(new User(), "dummy-password-never-used"); } public async Task RegisterAsync(RegisterDto dto) { if (dto.Password.Length < 12 || dto.Password.Length > 256) { throw new InvalidOperationException("비밀번호는 12자 이상 256자 이하여야 합니다."); } await BootstrapLock.WaitAsync(); try { var normalizedEmail = dto.Email.Trim().ToLowerInvariant(); var existing = await _db.Users.FirstOrDefaultAsync(u => u.Email.ToLower() == normalizedEmail); if (existing != null) { throw new InvalidOperationException("이미 등록된 이메일 주소입니다."); } if (await _db.Users.AnyAsync(u => u.IsActive)) { throw new InvalidOperationException("관리자 초기 등록이 이미 완료되었습니다."); } var user = new User { Email = normalizedEmail, Role = "SuperAdmin", CreatedAt = DateTime.UtcNow, IsActive = true }; user.PasswordHash = _passwordHasher.HashPassword(user, dto.Password); _db.Users.Add(user); await _db.SaveChangesAsync(); return GenerateToken(user); } finally { BootstrapLock.Release(); } } public async Task LoginAsync(LoginDto dto) { var normalizedEmail = dto.Email.Trim().ToLowerInvariant(); var user = await _db.Users.FirstOrDefaultAsync(u => u.Email.ToLower() == normalizedEmail); if (user == null) { _passwordHasher.VerifyHashedPassword(new User(), _dummyPasswordHash, dto.Password); throw new UnauthorizedAccessException("이메일 또는 비밀번호가 올바르지 않습니다."); } PasswordVerificationResult verification; try { verification = _passwordHasher.VerifyHashedPassword(user, user.PasswordHash, dto.Password); } catch (FormatException) { verification = PasswordVerificationResult.Failed; } if (verification == PasswordVerificationResult.Failed) { throw new UnauthorizedAccessException("이메일 또는 비밀번호가 올바르지 않습니다."); } if (!user.IsActive) { throw new UnauthorizedAccessException("비활성화된 계정입니다. 관리자에게 문의하세요."); } user.LastLoginAt = DateTime.UtcNow; if (verification == PasswordVerificationResult.SuccessRehashNeeded) { user.PasswordHash = _passwordHasher.HashPassword(user, dto.Password); } await _db.SaveChangesAsync(); return GenerateToken(user); } public async Task GetUserByEmailAsync(string email) { var user = await _db.Users.AsNoTracking().FirstOrDefaultAsync(u => u.Email.ToLower() == email.ToLower()); if (user == null) return null; return new UserInfoDto(user.Id, user.Email, user.Role, user.CreatedAt, user.LastLoginAt, user.IsActive); } private AuthResponseDto GenerateToken(User user) { var secretKey = _config["JWT_SECRET"]; if (string.IsNullOrWhiteSpace(secretKey) || Encoding.UTF8.GetByteCount(secretKey) < 32) { throw new InvalidOperationException("JWT_SECRET must contain at least 32 non-whitespace bytes."); } var issuer = _config["JWT_ISSUER"]; if (string.IsNullOrWhiteSpace(issuer)) { throw new InvalidOperationException("JWT_ISSUER is required."); } var audience = _config["JWT_AUDIENCE"]; if (string.IsNullOrWhiteSpace(audience)) { throw new InvalidOperationException("JWT_AUDIENCE is required."); } var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var claims = new[] { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.Email, user.Email), new Claim(ClaimTypes.Role, user.Role) }; var expiresAt = DateTime.UtcNow.AddHours(8); var token = new JwtSecurityToken( issuer: issuer, audience: audience, claims: claims, expires: expiresAt, signingCredentials: creds ); var tokenHandler = new JwtSecurityTokenHandler(); return new AuthResponseDto(tokenHandler.WriteToken(token), user.Email, user.Role, expiresAt); } }