Template
mirror of
https://github.com/egmont11/ASP.Net-Core-MVC-Template.git
synced 2026-07-24 07:09:56 +02:00
Changed Auth to use Identity, with easy to set passsword requirenments via appsettings.json
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TemplateWeb.Interfaces;
|
||||
using TemplateWeb.Entities;
|
||||
using TemplateWeb.Models;
|
||||
|
||||
namespace TemplateWeb.Controllers;
|
||||
@@ -8,144 +11,189 @@ namespace TemplateWeb.Controllers;
|
||||
public class AuthController : Controller
|
||||
{
|
||||
private readonly ILogger<AuthController> _logger;
|
||||
private readonly IAuthService _authService;
|
||||
|
||||
public AuthController(ILogger<AuthController> logger, IAuthService authService)
|
||||
private readonly UserManager<UserEntity> _userManager;
|
||||
private readonly SignInManager<UserEntity> _signInManager;
|
||||
|
||||
public AuthController(
|
||||
ILogger<AuthController> logger,
|
||||
UserManager<UserEntity> userManager,
|
||||
SignInManager<UserEntity> signInManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_authService = authService;
|
||||
_userManager = userManager;
|
||||
_signInManager = signInManager;
|
||||
}
|
||||
|
||||
|
||||
#region HttpGet
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult Login(string? returnUrl)
|
||||
{
|
||||
if (User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
return RedirectToAction("Index", "Home");
|
||||
}
|
||||
|
||||
return View(new AuthLoginViewModel { ReturnUrl = returnUrl });
|
||||
}
|
||||
|
||||
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult Register(string? returnUrl)
|
||||
{
|
||||
if (User.Identity?.IsAuthenticated == true)
|
||||
{
|
||||
return RedirectToAction("Index", "Home");
|
||||
}
|
||||
|
||||
return View(new AuthRegisterViewModel { ReturnUrl = returnUrl });
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult AccessDenied()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
public IActionResult AccessDenied() => 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 user = await _userManager.GetUserAsync(User);
|
||||
if (user == null) return RedirectToAction("Login");
|
||||
|
||||
var model = new ProfileViewModel
|
||||
return View(new ProfileViewModel
|
||||
{
|
||||
UserName = userResult.Data.UserName,
|
||||
Email = userResult.Data.Email
|
||||
};
|
||||
return View(model);
|
||||
UserName = user.UserName!,
|
||||
Email = user.Email!,
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region httpPost
|
||||
[HttpPost] [Authorize] [ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Profile(ProfileViewModel model)
|
||||
|
||||
#region HttpPost
|
||||
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Login(AuthLoginViewModel 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");
|
||||
// Support login by email or username
|
||||
var user = model.EmailOrUsername.Contains('@')
|
||||
? await _userManager.FindByEmailAsync(model.EmailOrUsername)
|
||||
: await _userManager.FindByNameAsync(model.EmailOrUsername);
|
||||
|
||||
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<IActionResult> Login(AuthLoginViewModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var userResult = await _authService.FindUserByEmailOrUsernameAsync(model.EmailOrUsername);
|
||||
if (!userResult.Success || userResult.Data == null)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, userResult.Message);
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var user = userResult.Data;
|
||||
if (!BCrypt.Net.BCrypt.Verify(model.Password, user.Password))
|
||||
if (user == null)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, "Invalid credentials.");
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var authResult = await _authService.AuthenticateAsync(user, model.RememberMe);
|
||||
if (!authResult.Success)
|
||||
|
||||
var result = await _signInManager.PasswordSignInAsync(
|
||||
user, model.Password, model.RememberMe, lockoutOnFailure: false);
|
||||
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, authResult.Message);
|
||||
ModelState.AddModelError(string.Empty, "Invalid credentials.");
|
||||
return View(model);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(model.ReturnUrl) && Url.IsLocalUrl(model.ReturnUrl))
|
||||
{
|
||||
return Redirect(model.ReturnUrl);
|
||||
}
|
||||
|
||||
|
||||
return RedirectToAction("Index", "Home");
|
||||
}
|
||||
|
||||
[HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken]
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Register(AuthRegisterViewModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
if (!ModelState.IsValid) return View(model);
|
||||
|
||||
var user = new UserEntity
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var result = await _authService.RegisterAsync(model);
|
||||
if (!result.Success)
|
||||
UserName = model.UserName.Trim(),
|
||||
Email = model.Email.Trim().ToLower(),
|
||||
};
|
||||
|
||||
var result = await _userManager.CreateAsync(user, model.Password);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, result.Message);
|
||||
foreach (var error in result.Errors)
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
return View(model);
|
||||
}
|
||||
|
||||
await _userManager.AddToRoleAsync(user, "User");
|
||||
_logger.LogInformation("User {UserName} registered.", user.UserName);
|
||||
|
||||
if (!string.IsNullOrEmpty(model.ReturnUrl) && Url.IsLocalUrl(model.ReturnUrl))
|
||||
{
|
||||
return Redirect(model.ReturnUrl);
|
||||
}
|
||||
|
||||
|
||||
return RedirectToAction("Login");
|
||||
}
|
||||
|
||||
|
||||
[HttpPost]
|
||||
[Authorize]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Profile(ProfileViewModel model)
|
||||
{
|
||||
if (!ModelState.IsValid) return View(model);
|
||||
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null) return RedirectToAction("Login");
|
||||
|
||||
// Update username
|
||||
if (user.UserName != model.UserName.Trim())
|
||||
{
|
||||
var setName = await _userManager.SetUserNameAsync(user, model.UserName.Trim());
|
||||
if (!setName.Succeeded)
|
||||
{
|
||||
foreach (var e in setName.Errors)
|
||||
ModelState.AddModelError(string.Empty, e.Description);
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
||||
// Update email
|
||||
if (user.Email != model.Email.Trim().ToLower())
|
||||
{
|
||||
var setEmail = await _userManager.SetEmailAsync(user, model.Email.Trim().ToLower());
|
||||
if (!setEmail.Succeeded)
|
||||
{
|
||||
foreach (var e in setEmail.Errors)
|
||||
ModelState.AddModelError(string.Empty, e.Description);
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
||||
// Change password (requires current password)
|
||||
if (!string.IsNullOrWhiteSpace(model.NewPassword))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(model.CurrentPassword))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.CurrentPassword), "Current password is required to set a new password.");
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var changePassword = await _userManager.ChangePasswordAsync(user, model.CurrentPassword, model.NewPassword);
|
||||
if (!changePassword.Succeeded)
|
||||
{
|
||||
foreach (var e in changePassword.Errors)
|
||||
ModelState.AddModelError(string.Empty, e.Description);
|
||||
return View(model);
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh cookie so the user stays logged in after security-stamp changes
|
||||
await _signInManager.RefreshSignInAsync(user);
|
||||
|
||||
TempData["SuccessMessage"] = "Profile updated successfully.";
|
||||
return RedirectToAction("Profile");
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
await _authService.LogoutAsync();
|
||||
await _signInManager.SignOutAsync();
|
||||
return RedirectToAction("Index", "Home");
|
||||
}
|
||||
|
||||
@@ -154,19 +202,12 @@ public class AuthController : Controller
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> DeleteAccount()
|
||||
{
|
||||
var userIdClaim = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier);
|
||||
if (userIdClaim == null) return RedirectToAction("Login");
|
||||
var user = await _userManager.GetUserAsync(User);
|
||||
if (user == null) return RedirectToAction("Login");
|
||||
|
||||
if (!int.TryParse(userIdClaim.Value, out var userId))
|
||||
{
|
||||
return RedirectToAction("Index", "Home");
|
||||
}
|
||||
|
||||
var userResult = await _authService.FindUserByEmailOrUsernameAsync(User.Identity?.Name ?? "");
|
||||
if (userResult is not { Success: true, Data: not null }) return RedirectToAction("Register");
|
||||
|
||||
await _authService.DeleteUserAsync(userResult.Data);
|
||||
await _authService.LogoutAsync();
|
||||
await _signInManager.SignOutAsync();
|
||||
await _userManager.DeleteAsync(user);
|
||||
_logger.LogInformation("User {UserId} deleted their account.", user.Id);
|
||||
|
||||
return RedirectToAction("Register");
|
||||
}
|
||||
@@ -175,16 +216,25 @@ public class AuthController : Controller
|
||||
[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 user = await _userManager.GetUserAsync(User);
|
||||
if (user == null) return RedirectToAction("Login");
|
||||
|
||||
var result = await _authService.GetUserDataAsync(userId);
|
||||
if (!result.Success || result.Data == null) return BadRequest(result.Message);
|
||||
var roles = await _userManager.GetRolesAsync(user);
|
||||
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(result.Data, new System.Text.Json.JsonSerializerOptions { WriteIndented = true });
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(json);
|
||||
var data = new
|
||||
{
|
||||
user.Id,
|
||||
user.UserName,
|
||||
user.Email,
|
||||
Roles = roles,
|
||||
ExportDate = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
return File(bytes, "application/json", $"user_data_{User.Identity?.Name}.json");
|
||||
var json = JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true });
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
|
||||
return File(bytes, "application/json", $"user_data_{user.UserName}.json");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user