Template
mirror of
https://github.com/egmont11/ASP.Net-Core-MVC-Template.git
synced 2026-07-24 07:09:56 +02:00
Added some GDPR things (data export, deletion, edit)
This commit is contained in:
@@ -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)
|
||||||
{
|
{
|
||||||
@if (User.IsInRole("Admin"))
|
<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">
|
||||||
<li class="nav-item">
|
<strong>@User.Identity.Name</strong>
|
||||||
<a class="nav-link text-primary fw-bold" asp-area="Admin" asp-controller="Dashboard" asp-action="Index">Admin Panel</a>
|
</a>
|
||||||
</li>
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
|
||||||
}
|
@if (User.IsInRole("Admin"))
|
||||||
<li class="nav-item">
|
{
|
||||||
<span class="nav-link text-dark">Hello, @User.Identity.Name!</span>
|
<li><a class="dropdown-item text-primary fw-bold" asp-area="Admin" asp-controller="Dashboard" asp-action="Index">Admin Panel</a></li>
|
||||||
</li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
<li class="nav-item">
|
}
|
||||||
<form asp-controller="Auth" asp-action="Logout" method="post" class="form-inline">
|
<li><a class="dropdown-item" asp-area="" asp-controller="Auth" asp-action="Profile">My Profile</a></li>
|
||||||
<button type="submit" class="nav-link btn btn-link text-dark">Logout</button>
|
<li><hr class="dropdown-divider"></li>
|
||||||
</form>
|
<li>
|
||||||
|
<form asp-area="" asp-controller="Auth" asp-action="Logout" method="post" class="dropdown-item p-0">
|
||||||
|
<button type="submit" class="btn btn-link dropdown-item">Logout</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</li>
|
</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user