feat(release): prepare 1.1.0 candidate
This commit is contained in:
parent
5a34f66981
commit
5205dcdfa9
736 changed files with 115667 additions and 12203 deletions
|
|
@ -1,13 +1,13 @@
|
|||
using System;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
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;
|
||||
|
|
@ -21,43 +21,81 @@ public interface IAuthService
|
|||
|
||||
public class AuthService : IAuthService
|
||||
{
|
||||
private static readonly SemaphoreSlim BootstrapLock = new(1, 1);
|
||||
private readonly AppDbContext _db;
|
||||
private readonly IConfiguration _config;
|
||||
private readonly PasswordHasher<User> _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<AuthResponseDto> RegisterAsync(RegisterDto dto)
|
||||
{
|
||||
var existing = await _db.Users.FirstOrDefaultAsync(u => u.Email.ToLower() == dto.Email.ToLower());
|
||||
if (existing != null)
|
||||
if (dto.Password.Length < 12 || dto.Password.Length > 256)
|
||||
{
|
||||
throw new InvalidOperationException("이미 등록된 이메일 주소입니다.");
|
||||
throw new InvalidOperationException("비밀번호는 12자 이상 256자 이하여야 합니다.");
|
||||
}
|
||||
|
||||
var isFirstUser = !await _db.Users.AnyAsync();
|
||||
var user = new User
|
||||
await BootstrapLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
Email = dto.Email.Trim().ToLower(),
|
||||
PasswordHash = HashPassword(dto.Password),
|
||||
Role = isFirstUser ? "Admin" : "User",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
IsActive = true
|
||||
};
|
||||
var normalizedEmail = dto.Email.Trim().ToLowerInvariant();
|
||||
var existing = await _db.Users.FirstOrDefaultAsync(u => u.Email.ToLower() == normalizedEmail);
|
||||
if (existing != null)
|
||||
{
|
||||
throw new InvalidOperationException("이미 등록된 이메일 주소입니다.");
|
||||
}
|
||||
|
||||
_db.Users.Add(user);
|
||||
await _db.SaveChangesAsync();
|
||||
if (await _db.Users.AnyAsync(u => u.IsActive))
|
||||
{
|
||||
throw new InvalidOperationException("관리자 초기 등록이 이미 완료되었습니다.");
|
||||
}
|
||||
|
||||
return GenerateToken(user);
|
||||
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<AuthResponseDto> LoginAsync(LoginDto dto)
|
||||
{
|
||||
var user = await _db.Users.FirstOrDefaultAsync(u => u.Email.ToLower() == dto.Email.ToLower());
|
||||
if (user == null || !VerifyPassword(dto.Password, user.PasswordHash))
|
||||
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("이메일 또는 비밀번호가 올바르지 않습니다.");
|
||||
}
|
||||
|
|
@ -68,6 +106,10 @@ public class AuthService : IAuthService
|
|||
}
|
||||
|
||||
user.LastLoginAt = DateTime.UtcNow;
|
||||
if (verification == PasswordVerificationResult.SuccessRehashNeeded)
|
||||
{
|
||||
user.PasswordHash = _passwordHasher.HashPassword(user, dto.Password);
|
||||
}
|
||||
await _db.SaveChangesAsync();
|
||||
|
||||
return GenerateToken(user);
|
||||
|
|
@ -82,7 +124,21 @@ public class AuthService : IAuthService
|
|||
|
||||
private AuthResponseDto GenerateToken(User user)
|
||||
{
|
||||
var secretKey = _config["Jwt:SecretKey"] ?? "D3ROVoice_Super_Secure_Secret_Key_2026_Key!";
|
||||
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);
|
||||
|
||||
|
|
@ -93,11 +149,11 @@ public class AuthService : IAuthService
|
|||
new Claim(ClaimTypes.Role, user.Role)
|
||||
};
|
||||
|
||||
var expiresAt = DateTime.UtcNow.AddDays(30);
|
||||
var expiresAt = DateTime.UtcNow.AddHours(8);
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: _config["Jwt:Issuer"] ?? "D3ROVoiceApi",
|
||||
audience: _config["Jwt:Audience"] ?? "D3ROVoiceClient",
|
||||
issuer: issuer,
|
||||
audience: audience,
|
||||
claims: claims,
|
||||
expires: expiresAt,
|
||||
signingCredentials: creds
|
||||
|
|
@ -107,15 +163,4 @@ public class AuthService : IAuthService
|
|||
return new AuthResponseDto(tokenHandler.WriteToken(token), user.Email, user.Role, expiresAt);
|
||||
}
|
||||
|
||||
public static string HashPassword(string password)
|
||||
{
|
||||
using var sha256 = SHA256.Create();
|
||||
var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(password + "D3RO_SALT_2026"));
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
|
||||
private static bool VerifyPassword(string password, string hash)
|
||||
{
|
||||
return HashPassword(password) == hash;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue