dramaling-vocab-learning/backend/DramaLing.Api/Controllers/AuthController.cs

517 lines
18 KiB
C#

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;
[Route("api/[controller]")]
public class AuthController : BaseController
{
private readonly DramaLingDbContext _context;
public AuthController(
DramaLingDbContext context,
IAuthService authService,
ILogger<AuthController> logger) : base(logger, authService)
{
_context = context;
}
[HttpPost("register")]
public async Task<IActionResult> Register([FromBody] RegisterRequest request)
{
try
{
_logger.LogInformation("Registration attempt for user: {Username}, Email: {Email}",
request.Username, request.Email);
// 驗證請求
if (!ModelState.IsValid)
{
_logger.LogWarning("Invalid model state for registration: {@ModelErrors}",
ModelState.Where(x => x.Value?.Errors.Count > 0)
.ToDictionary(x => x.Key, x => x.Value?.Errors.Select(e => e.ErrorMessage)));
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. Request: {@Request}", new {
Username = request.Username,
Email = request.Email,
StackTrace = ex.StackTrace,
InnerException = ex.InnerException?.Message
});
return StatusCode(500, new
{
Success = false,
Error = "Registration failed",
Details = ex.Message,
Timestamp = DateTime.UtcNow
});
}
}
[HttpPost("login")]
public async Task<ActionResult> Login([FromBody] LoginRequest request)
{
try
{
_logger.LogInformation("Login attempt for email: {Email}", request.Email);
// 驗證請求
if (!ModelState.IsValid)
{
_logger.LogWarning("Invalid model state for login: {@ModelErrors}",
ModelState.Where(x => x.Value?.Errors.Count > 0)
.ToDictionary(x => x.Key, x => x.Value?.Errors.Select(e => e.ErrorMessage)));
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. Request: {@Request}", new {
Email = request.Email,
StackTrace = ex.StackTrace,
InnerException = ex.InnerException?.Message
});
return StatusCode(500, new
{
Success = false,
Error = "Login failed",
Details = ex.Message,
Timestamp = DateTime.UtcNow
});
}
}
private string GenerateJwtToken(User user)
{
var jwtSecret = Environment.GetEnvironmentVariable("DRAMALING_SUPABASE_JWT_SECRET")
?? Environment.GetEnvironmentVariable("DRAMALING_JWT_SECRET")
?? "dev-secret-minimum-32-characters-long-for-jwt-signing-in-development-mode-only";
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<ActionResult> 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<ActionResult> 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<ActionResult> 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<ActionResult> 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
});
}
}
/// <summary>
/// 檢查用戶認證狀態 (無需資料庫查詢的快速檢查)
/// </summary>
[HttpGet("status")]
[Authorize]
public async Task<ActionResult> 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<string, object>? 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; }
}