Added some GDPR things (data export, deletion, edit)

This commit is contained in:
Matěj Kubíček
2026-04-18 19:04:22 +02:00
parent 982093ff7a
commit ee7aa57383
6 changed files with 222 additions and 15 deletions
@@ -44,9 +44,43 @@ public class AuthController : Controller
{ {
return View(); return View();
} }
[HttpGet]
[Authorize]
public async Task<IActionResult> 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 #endregion
#region httpPost #region httpPost
[HttpPost] [Authorize] [ValidateAntiForgeryToken]
public async Task<IActionResult> 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] [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken]
public async Task<IActionResult> Login(AuthLoginViewModel model) public async Task<IActionResult> Login(AuthLoginViewModel model)
{ {
@@ -136,5 +170,21 @@ public class AuthController : Controller
return RedirectToAction("Register"); return RedirectToAction("Register");
} }
[HttpGet]
[Authorize]
public async Task<IActionResult> 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 #endregion
} }
@@ -10,4 +10,6 @@ public interface IAuthService
Task<ServiceResult<UserModel>> RegisterAsync(AuthRegisterViewModel model); Task<ServiceResult<UserModel>> RegisterAsync(AuthRegisterViewModel model);
Task LogoutAsync(); Task LogoutAsync();
Task<ServiceResult> DeleteUserAsync(UserModel user); Task<ServiceResult> DeleteUserAsync(UserModel user);
Task<ServiceResult> UpdateProfileAsync(int userId, ProfileViewModel model);
Task<ServiceResult<object>> GetUserDataAsync(int userId);
} }
@@ -132,4 +132,52 @@ public class AuthService : IAuthService
return ServiceResult.Failure("An error occurred while deleting the user."); return ServiceResult.Failure("An error occurred while deleting the user.");
} }
} }
public async Task<ServiceResult> 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<ServiceResult<object>> GetUserDataAsync(int userId)
{
var user = await _context.Users.FindAsync(userId);
if (user == null) return ServiceResult<object>.Failure("User not found.");
var data = new
{
user.Id,
user.UserName,
user.Email,
Role = user.Role.ToString(),
ExportDate = DateTime.UtcNow
};
return ServiceResult<object>.Ok(data);
}
} }
@@ -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; }
}
@@ -0,0 +1,76 @@
@model ProfileViewModel
@{
ViewData["Title"] = "My Profile";
}
<div class="row justify-content-center mt-5">
<div class="col-md-6">
@if (TempData["SuccessMessage"] != null)
{
<div class="alert alert-success">@TempData["SuccessMessage"]</div>
}
<div class="card shadow-sm mb-4">
<div class="card-body p-4">
<h1 class="card-title text-center mb-4">My Profile</h1>
<form asp-action="Profile">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="mb-3">
<label asp-for="UserName" class="form-label"></label>
<input asp-for="UserName" class="form-control" />
<span asp-validation-for="UserName" class="text-danger small"></span>
</div>
<div class="mb-3">
<label asp-for="Email" class="form-label"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger small"></span>
</div>
<hr />
<p class="text-muted small">Change Password (optional)</p>
<div class="mb-3">
<label asp-for="NewPassword" class="form-label"></label>
<input asp-for="NewPassword" class="form-control" />
<span asp-validation-for="NewPassword" class="text-danger small"></span>
</div>
<div class="mb-3">
<label asp-for="ConfirmPassword" class="form-label"></label>
<input asp-for="ConfirmPassword" class="form-control" />
<span asp-validation-for="ConfirmPassword" class="text-danger small"></span>
</div>
<div class="d-grid mt-4">
<button type="submit" class="btn btn-primary">Update Profile</button>
</div>
</form>
</div>
</div>
<div class="card border-danger shadow-sm">
<div class="card-body p-4">
<h3 class="text-danger">Privacy & Data</h3>
<p class="text-muted small">Manage your personal data and account status.</p>
<div class="d-grid gap-2">
<a asp-action="DownloadData" class="btn btn-outline-info">Download My Data (JSON)</a>
<form asp-action="DeleteAccount" method="post" onsubmit="return confirm('Are you absolutely sure you want to delete your account? This action cannot be undone.');">
@Html.AntiForgeryToken()
<div class="d-grid">
<button type="submit" class="btn btn-outline-danger">Delete My Account</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@section Scripts {
@{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
}
@@ -30,28 +30,33 @@
<ul class="navbar-nav"> <ul class="navbar-nav">
@if (User.Identity?.IsAuthenticated == true) @if (User.Identity?.IsAuthenticated == true)
{ {
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle text-dark" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<strong>@User.Identity.Name</strong>
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
@if (User.IsInRole("Admin")) @if (User.IsInRole("Admin"))
{ {
<li class="nav-item"> <li><a class="dropdown-item text-primary fw-bold" asp-area="Admin" asp-controller="Dashboard" asp-action="Index">Admin Panel</a></li>
<a class="nav-link text-primary fw-bold" asp-area="Admin" asp-controller="Dashboard" asp-action="Index">Admin Panel</a> <li><hr class="dropdown-divider"></li>
</li>
} }
<li class="nav-item"> <li><a class="dropdown-item" asp-area="" asp-controller="Auth" asp-action="Profile">My Profile</a></li>
<span class="nav-link text-dark">Hello, @User.Identity.Name!</span> <li><hr class="dropdown-divider"></li>
</li> <li>
<li class="nav-item"> <form asp-area="" asp-controller="Auth" asp-action="Logout" method="post" class="dropdown-item p-0">
<form asp-controller="Auth" asp-action="Logout" method="post" class="form-inline"> <button type="submit" class="btn btn-link dropdown-item">Logout</button>
<button type="submit" class="nav-link btn btn-link text-dark">Logout</button>
</form> </form>
</li> </li>
</ul>
</li>
} }
else else
{ {
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-dark" asp-controller="Auth" asp-action="Register">Register</a> <a class="nav-link text-dark" asp-area="" asp-controller="Auth" asp-action="Register">Register</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-dark" asp-controller="Auth" asp-action="Login">Login</a> <a class="nav-link text-dark" asp-area="" asp-controller="Auth" asp-action="Login">Login</a>
</li> </li>
} }
</ul> </ul>