using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using DramaLing.Api.Data; using DramaLing.Api.Models.Entities; using DramaLing.Api.Services; using Microsoft.AspNetCore.Authorization; using System.ComponentModel.DataAnnotations; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using Microsoft.IdentityModel.Tokens; namespace DramaLing.Api.Controllers; [ApiController] [Route("api/[controller]")] public class AuthController : ControllerBase { private readonly DramaLingDbContext _context; private readonly IAuthService _authService; private readonly ILogger _logger; public AuthController( DramaLingDbContext context, IAuthService authService, ILogger logger) { _context = context; _authService = authService; _logger = logger; } [HttpPost("register")] public async Task Register([FromBody] RegisterRequest request) { try { // 驗證請求 if (!ModelState.IsValid) return BadRequest(new { Success = false, Error = "Invalid request data" }); // 檢查Email是否已存在 if (await _context.Users.AnyAsync(u => u.Email == request.Email)) return BadRequest(new { Success = false, Error = "Email already exists" }); // 檢查用戶名是否已存在 if (await _context.Users.AnyAsync(u => u.Username == request.Username)) return BadRequest(new { Success = false, Error = "Username already exists" }); // 雜湊密碼 var passwordHash = BCrypt.Net.BCrypt.HashPassword(request.Password); // 建立新用戶 var user = new User { Id = Guid.NewGuid(), Username = request.Username, Email = request.Email, PasswordHash = passwordHash, DisplayName = request.Username, // 預設使用用戶名作為顯示名稱 CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; _context.Users.Add(user); await _context.SaveChangesAsync(); // 生成JWT Token var token = GenerateJwtToken(user); _logger.LogInformation("User registered successfully: {UserId}", user.Id); return Ok(new { Success = true, Data = new { Token = token, User = new { user.Id, user.Username, user.Email, user.DisplayName, user.AvatarUrl, user.SubscriptionType } } }); } catch (Exception ex) { _logger.LogError(ex, "Error during user registration"); return StatusCode(500, new { Success = false, Error = "Registration failed", Timestamp = DateTime.UtcNow }); } } [HttpPost("login")] public async Task Login([FromBody] LoginRequest request) { try { // 驗證請求 if (!ModelState.IsValid) return BadRequest(new { Success = false, Error = "Invalid request data" }); // 查找用戶 var user = await _context.Users.FirstOrDefaultAsync(u => u.Email == request.Email); if (user == null) return Unauthorized(new { Success = false, Error = "Invalid email or password" }); // 驗證密碼 if (!BCrypt.Net.BCrypt.Verify(request.Password, user.PasswordHash)) return Unauthorized(new { Success = false, Error = "Invalid email or password" }); // 生成JWT Token var token = GenerateJwtToken(user); _logger.LogInformation("User logged in successfully: {UserId}", user.Id); return Ok(new { Success = true, Data = new { Token = token, User = new { user.Id, user.Username, user.Email, user.DisplayName, user.AvatarUrl, user.SubscriptionType } } }); } catch (Exception ex) { _logger.LogError(ex, "Error during user login"); return StatusCode(500, new { Success = false, Error = "Login failed", Timestamp = DateTime.UtcNow }); } } private string GenerateJwtToken(User user) { var jwtSecret = Environment.GetEnvironmentVariable("DRAMALING_JWT_SECRET") ?? "your-super-secret-jwt-key-that-should-be-at-least-256-bits-long"; var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.UTF8.GetBytes(jwtSecret); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim("sub", user.Id.ToString()), new Claim("email", user.Email), new Claim("username", user.Username), new Claim("name", user.DisplayName ?? user.Username) }), Expires = DateTime.UtcNow.AddDays(7), Issuer = Environment.GetEnvironmentVariable("DRAMALING_SUPABASE_URL") ?? "http://localhost:5000", Audience = "authenticated", SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256) }; var token = tokenHandler.CreateToken(tokenDescriptor); return tokenHandler.WriteToken(token); } [HttpGet("profile")] [Authorize] public async Task GetProfile() { try { var userId = await _authService.GetUserIdFromTokenAsync(Request.Headers.Authorization); if (userId == null) return Unauthorized(new { Success = false, Error = "Invalid token" }); var user = await _context.Users.FirstOrDefaultAsync(u => u.Id == userId); // 如果用戶不存在,從 JWT 令牌建立基本資料 if (user == null) { var claimsPrincipal = await _authService.ValidateTokenAsync( Request.Headers.Authorization.ToString()["Bearer ".Length..].Trim()); if (claimsPrincipal == null) return Unauthorized(new { Success = false, Error = "Invalid token" }); var email = claimsPrincipal.FindFirst("email")?.Value ?? ""; var displayName = claimsPrincipal.FindFirst("name")?.Value ?? claimsPrincipal.FindFirst("user_metadata")?.Value ?? email.Split('@')[0]; user = new User { Id = userId.Value, Email = email, DisplayName = displayName, SubscriptionType = "free" }; _context.Users.Add(user); await _context.SaveChangesAsync(); _logger.LogInformation("Created new user profile for {UserId}", userId); } return Ok(new { Success = true, Data = new { user.Id, user.Email, user.DisplayName, user.AvatarUrl, user.SubscriptionType, user.CreatedAt } }); } catch (Exception ex) { _logger.LogError(ex, "Error fetching user profile"); return StatusCode(500, new { Success = false, Error = "Failed to fetch profile", Timestamp = DateTime.UtcNow }); } } [HttpPut("profile")] [Authorize] public async Task UpdateProfile([FromBody] UpdateProfileRequest request) { try { var userId = await _authService.GetUserIdFromTokenAsync(Request.Headers.Authorization); if (userId == null) return Unauthorized(new { Success = false, Error = "Invalid token" }); var user = await _context.Users.FirstOrDefaultAsync(u => u.Id == userId); if (user == null) return NotFound(new { Success = false, Error = "User not found" }); // 更新用戶資料 if (!string.IsNullOrWhiteSpace(request.DisplayName)) { if (request.DisplayName.Length > 100) return BadRequest(new { Success = false, Error = "Display name must be less than 100 characters" }); user.DisplayName = request.DisplayName.Trim(); } if (request.AvatarUrl != null) user.AvatarUrl = request.AvatarUrl?.Trim(); if (request.Preferences != null) user.Preferences = request.Preferences; user.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); return Ok(new { Success = true, Data = user, Message = "Profile updated successfully" }); } catch (Exception ex) { _logger.LogError(ex, "Error updating user profile"); return StatusCode(500, new { Success = false, Error = "Failed to update profile", Timestamp = DateTime.UtcNow }); } } [HttpGet("settings")] [Authorize] public async Task GetSettings() { try { var userId = await _authService.GetUserIdFromTokenAsync(Request.Headers.Authorization); if (userId == null) return Unauthorized(new { Success = false, Error = "Invalid token" }); var settings = await _context.UserSettings.FirstOrDefaultAsync(s => s.UserId == userId); // 如果沒有設定,建立預設設定 if (settings == null) { settings = new UserSettings { Id = Guid.NewGuid(), UserId = userId.Value, DailyGoal = 20, ReminderTime = new TimeOnly(9, 0), ReminderEnabled = true, DifficultyPreference = "balanced", AutoPlayAudio = true, ShowPronunciation = true }; _context.UserSettings.Add(settings); await _context.SaveChangesAsync(); _logger.LogInformation("Created default settings for user {UserId}", userId); } return Ok(new { Success = true, Data = settings }); } catch (Exception ex) { _logger.LogError(ex, "Error fetching user settings"); return StatusCode(500, new { Success = false, Error = "Failed to fetch settings", Timestamp = DateTime.UtcNow }); } } [HttpPut("settings")] [Authorize] public async Task UpdateSettings([FromBody] UpdateSettingsRequest request) { try { var userId = await _authService.GetUserIdFromTokenAsync(Request.Headers.Authorization); if (userId == null) return Unauthorized(new { Success = false, Error = "Invalid token" }); var settings = await _context.UserSettings.FirstOrDefaultAsync(s => s.UserId == userId); if (settings == null) return NotFound(new { Success = false, Error = "Settings not found" }); // 驗證並更新設定 if (request.DailyGoal.HasValue) { if (request.DailyGoal < 1 || request.DailyGoal > 100) return BadRequest(new { Success = false, Error = "Daily goal must be between 1 and 100" }); settings.DailyGoal = request.DailyGoal.Value; } if (request.ReminderTime.HasValue) settings.ReminderTime = request.ReminderTime.Value; if (request.ReminderEnabled.HasValue) settings.ReminderEnabled = request.ReminderEnabled.Value; if (!string.IsNullOrEmpty(request.DifficultyPreference)) { if (!new[] { "conservative", "balanced", "aggressive" }.Contains(request.DifficultyPreference)) return BadRequest(new { Success = false, Error = "Invalid difficulty preference" }); settings.DifficultyPreference = request.DifficultyPreference; } if (request.AutoPlayAudio.HasValue) settings.AutoPlayAudio = request.AutoPlayAudio.Value; if (request.ShowPronunciation.HasValue) settings.ShowPronunciation = request.ShowPronunciation.Value; settings.UpdatedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); return Ok(new { Success = true, Data = settings, Message = "Settings updated successfully" }); } catch (Exception ex) { _logger.LogError(ex, "Error updating user settings"); return StatusCode(500, new { Success = false, Error = "Failed to update settings", Timestamp = DateTime.UtcNow }); } } /// /// 檢查用戶認證狀態 (無需資料庫查詢的快速檢查) /// [HttpGet("status")] [Authorize] public async Task GetAuthStatus() { try { var userId = await _authService.GetUserIdFromTokenAsync(Request.Headers.Authorization); if (userId == null) return Unauthorized(new { Success = false, Error = "Invalid token" }); return Ok(new { Success = true, Data = new { IsAuthenticated = true, UserId = userId, Timestamp = DateTime.UtcNow } }); } catch (Exception ex) { _logger.LogError(ex, "Error checking auth status"); return StatusCode(500, new { Success = false, Error = "Failed to check auth status", Timestamp = DateTime.UtcNow }); } } } // Request DTOs public class RegisterRequest { [Required(ErrorMessage = "Username is required")] [StringLength(50, MinimumLength = 3, ErrorMessage = "Username must be between 3 and 50 characters")] public string Username { get; set; } = string.Empty; [Required(ErrorMessage = "Email is required")] [EmailAddress(ErrorMessage = "Invalid email format")] public string Email { get; set; } = string.Empty; [Required(ErrorMessage = "Password is required")] [StringLength(100, MinimumLength = 8, ErrorMessage = "Password must be at least 8 characters")] public string Password { get; set; } = string.Empty; } public class LoginRequest { [Required(ErrorMessage = "Email is required")] [EmailAddress(ErrorMessage = "Invalid email format")] public string Email { get; set; } = string.Empty; [Required(ErrorMessage = "Password is required")] public string Password { get; set; } = string.Empty; } public class UpdateProfileRequest { public string? DisplayName { get; set; } public string? AvatarUrl { get; set; } public Dictionary? Preferences { get; set; } } public class UpdateSettingsRequest { public int? DailyGoal { get; set; } public TimeOnly? ReminderTime { get; set; } public bool? ReminderEnabled { get; set; } public string? DifficultyPreference { get; set; } public bool? AutoPlayAudio { get; set; } public bool? ShowPronunciation { get; set; } }