diff --git a/Template/TemplateWeb/Controllers/AuthController.cs b/Template/TemplateWeb/Controllers/AuthController.cs index 1a57f77..8b10f8b 100644 --- a/Template/TemplateWeb/Controllers/AuthController.cs +++ b/Template/TemplateWeb/Controllers/AuthController.cs @@ -44,9 +44,43 @@ public class AuthController : Controller { return View(); } + + [HttpGet] + [Authorize] + public async Task Profile() + { + var userResult = await _authService.FindUserByEmailOrUsernameAsync(User.Identity?.Name ?? ""); + if (!userResult.Success || userResult.Data == null) return RedirectToAction("Login"); + + var model = new ProfileViewModel + { + UserName = userResult.Data.UserName, + Email = userResult.Data.Email + }; + return View(model); + } #endregion #region httpPost + [HttpPost] [Authorize] [ValidateAntiForgeryToken] + public async Task Profile(ProfileViewModel model) + { + if (!ModelState.IsValid) return View(model); + + var userIdClaim = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier); + if (userIdClaim == null || !int.TryParse(userIdClaim.Value, out int userId)) return RedirectToAction("Login"); + + var result = await _authService.UpdateProfileAsync(userId, model); + if (!result.Success) + { + ModelState.AddModelError(string.Empty, result.Message); + return View(model); + } + + TempData["SuccessMessage"] = "Profile updated successfully. Please log in again if you changed your username."; + return RedirectToAction("Profile"); + } + [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task Login(AuthLoginViewModel model) { @@ -136,5 +170,21 @@ public class AuthController : Controller return RedirectToAction("Register"); } + + [HttpGet] + [Authorize] + public async Task DownloadData() + { + var userIdClaim = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier); + if (userIdClaim == null || !int.TryParse(userIdClaim.Value, out int userId)) return RedirectToAction("Login"); + + var result = await _authService.GetUserDataAsync(userId); + if (!result.Success || result.Data == null) return BadRequest(result.Message); + + var json = System.Text.Json.JsonSerializer.Serialize(result.Data, new System.Text.Json.JsonSerializerOptions { WriteIndented = true }); + var bytes = System.Text.Encoding.UTF8.GetBytes(json); + + return File(bytes, "application/json", $"user_data_{User.Identity?.Name}.json"); + } #endregion } diff --git a/Template/TemplateWeb/Interfaces/IAuthService.cs b/Template/TemplateWeb/Interfaces/IAuthService.cs index 168b603..ac33be9 100644 --- a/Template/TemplateWeb/Interfaces/IAuthService.cs +++ b/Template/TemplateWeb/Interfaces/IAuthService.cs @@ -10,4 +10,6 @@ public interface IAuthService Task> RegisterAsync(AuthRegisterViewModel model); Task LogoutAsync(); Task DeleteUserAsync(UserModel user); + Task UpdateProfileAsync(int userId, ProfileViewModel model); + Task> GetUserDataAsync(int userId); } diff --git a/Template/TemplateWeb/Services/AuthService.cs b/Template/TemplateWeb/Services/AuthService.cs index 1f02d8d..c627956 100644 --- a/Template/TemplateWeb/Services/AuthService.cs +++ b/Template/TemplateWeb/Services/AuthService.cs @@ -132,4 +132,52 @@ public class AuthService : IAuthService return ServiceResult.Failure("An error occurred while deleting the user."); } } + + public async Task UpdateProfileAsync(int userId, ProfileViewModel model) + { + var user = await _context.Users.FindAsync(userId); + if (user == null) return ServiceResult.Failure("User not found."); + + var isTaken = await _context.Users.AnyAsync(u => u.Id != userId && + (u.UserName.ToLower() == model.UserName.Trim().ToLower() || + u.Email.ToLower() == model.Email.Trim().ToLower())); + + if (isTaken) return ServiceResult.Failure("Username or Email already exists."); + + user.UserName = model.UserName.Trim(); + user.Email = model.Email.Trim().ToLower(); + + if (!string.IsNullOrWhiteSpace(model.NewPassword)) + { + user.Password = BCrypt.Net.BCrypt.HashPassword(model.NewPassword); + } + + try + { + await _context.SaveChangesAsync(); + return ServiceResult.Ok(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating profile for user {UserId}", userId); + return ServiceResult.Failure("An error occurred while saving changes."); + } + } + + public async Task> GetUserDataAsync(int userId) + { + var user = await _context.Users.FindAsync(userId); + if (user == null) return ServiceResult.Failure("User not found."); + + var data = new + { + user.Id, + user.UserName, + user.Email, + Role = user.Role.ToString(), + ExportDate = DateTime.UtcNow + }; + + return ServiceResult.Ok(data); + } } diff --git a/Template/TemplateWeb/ViewModels/ProfileViewModel.cs b/Template/TemplateWeb/ViewModels/ProfileViewModel.cs new file mode 100644 index 0000000..65c8d2a --- /dev/null +++ b/Template/TemplateWeb/ViewModels/ProfileViewModel.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations; + +namespace TemplateWeb.Models; + +public class ProfileViewModel +{ + [Required] + [Display(Name = "Username")] + [StringLength(50, MinimumLength = 3)] + public string UserName { get; set; } = string.Empty; + + [Required] + [EmailAddress] + [Display(Name = "Email")] + public string Email { get; set; } = string.Empty; + + [DataType(DataType.Password)] + [Display(Name = "New Password (leave blank to keep current)")] + [MinLength(5)] + public string? NewPassword { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm New Password")] + [Compare("NewPassword")] + public string? ConfirmPassword { get; set; } +} diff --git a/Template/TemplateWeb/Views/Auth/Profile.cshtml b/Template/TemplateWeb/Views/Auth/Profile.cshtml new file mode 100644 index 0000000..f331fdb --- /dev/null +++ b/Template/TemplateWeb/Views/Auth/Profile.cshtml @@ -0,0 +1,76 @@ +@model ProfileViewModel +@{ + ViewData["Title"] = "My Profile"; +} + +
+
+ @if (TempData["SuccessMessage"] != null) + { +
@TempData["SuccessMessage"]
+ } + +
+
+

My Profile

+ +
+
+ +
+ + + +
+ +
+ + + +
+ +
+

Change Password (optional)

+ +
+ + + +
+ +
+ + + +
+ +
+ +
+
+
+
+ +
+
+

Privacy & Data

+

Manage your personal data and account status.

+ +
+ Download My Data (JSON) + +
+ @Html.AntiForgeryToken() +
+ +
+
+
+
+
+
+
+ +@section Scripts { + @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } +} diff --git a/Template/TemplateWeb/Views/Shared/_Layout.cshtml b/Template/TemplateWeb/Views/Shared/_Layout.cshtml index a5c7fc1..337ff26 100644 --- a/Template/TemplateWeb/Views/Shared/_Layout.cshtml +++ b/Template/TemplateWeb/Views/Shared/_Layout.cshtml @@ -30,28 +30,33 @@