InitEshop+ddd+ca

This commit is contained in:
Matěj Kubíček
2026-07-16 18:33:05 +02:00
parent ffd8a087c8
commit 7e080eaef9
144 changed files with 972 additions and 1339 deletions
+15
View File
@@ -0,0 +1,15 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/projectSettingsUpdater.xml
/.idea.Eshop.iml
/contentModel.xml
/modules.xml
# Editor-based HTTP Client requests
/httpRequests/
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>
@@ -0,0 +1,9 @@
namespace Eshop.Application.Auth;
public sealed class SignInResultDto
{
public bool Succeeded { get; private init; }
public static SignInResultDto Success => new() { Succeeded = true };
public static SignInResultDto Failure => new() { Succeeded = false };
}
@@ -0,0 +1,8 @@
namespace Eshop.Application.Auth;
public sealed record UserDataExportDto(
string Id,
string UserName,
string Email,
IReadOnlyList<string> Roles,
DateTime ExportDate);
@@ -0,0 +1,3 @@
namespace Eshop.Application.Auth;
public sealed record UserDto(string Id, string UserName, string Email);
@@ -0,0 +1,3 @@
namespace Eshop.Application.Auth;
public sealed record UserProfileDto(string UserName, string Email);
@@ -0,0 +1,6 @@
namespace Eshop.Application.Auth;
public sealed record UserWithRolesDto(
UserDto User,
IReadOnlyList<string> Roles,
IReadOnlyList<string> AllRoles);
@@ -0,0 +1,12 @@
namespace Eshop.Application.Auth;
public interface IAuthenticationService
{
Task<SignInResultDto> SignInAsync(
string emailOrUsername,
string password,
bool rememberMe,
CancellationToken cancellationToken = default);
Task SignOutAsync(CancellationToken cancellationToken = default);
}
@@ -0,0 +1,29 @@
using Eshop.Application.Common;
namespace Eshop.Application.Auth;
public interface IUserAccountService
{
Task<ServiceResult> RegisterAsync(
string userName,
string email,
string password,
CancellationToken cancellationToken = default);
Task<UserProfileDto?> GetProfileAsync(
string userId,
CancellationToken cancellationToken = default);
Task<ServiceResult> UpdateProfileAsync(
string userId,
UpdateProfileRequest request,
CancellationToken cancellationToken = default);
Task<ServiceResult> DeleteAccountAsync(
string userId,
CancellationToken cancellationToken = default);
Task<UserDataExportDto?> ExportUserDataAsync(
string userId,
CancellationToken cancellationToken = default);
}
@@ -0,0 +1,18 @@
using Eshop.Application.Common;
namespace Eshop.Application.Auth;
public interface IUserAdministrationService
{
Task<IReadOnlyList<UserWithRolesDto>> GetAllUsersWithRolesAsync(
CancellationToken cancellationToken = default);
Task<UserWithRolesDto?> GetUserWithRolesAsync(
string userId,
CancellationToken cancellationToken = default);
Task<ServiceResult> UpdateUserRolesAsync(
string userId,
IReadOnlyList<string> selectedRoles,
CancellationToken cancellationToken = default);
}
@@ -0,0 +1,7 @@
namespace Eshop.Application.Auth;
public sealed record UpdateProfileRequest(
string UserName,
string Email,
string? CurrentPassword,
string? NewPassword);
@@ -0,0 +1,28 @@
namespace Eshop.Application.Common;
public class ServiceResult
{
public bool Success { get; protected set; }
public IReadOnlyList<string> Errors { get; protected set; } = [];
public static ServiceResult Ok() => new() { Success = true };
public static ServiceResult Failure(string message) =>
new() { Success = false, Errors = [message] };
public static ServiceResult Failure(IEnumerable<string> errors) =>
new() { Success = false, Errors = errors.ToList() };
}
public class ServiceResult<T> : ServiceResult
{
public T? Data { get; private set; }
public static ServiceResult<T> Ok(T data) => new() { Success = true, Data = data };
public new static ServiceResult<T> Failure(string message) =>
new() { Success = false, Errors = [message] };
public new static ServiceResult<T> Failure(IEnumerable<string> errors) =>
new() { Success = false, Errors = errors.ToList() };
}
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Eshop.Domain\Eshop.Domain.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,6 @@
namespace Eshop.Domain.Common;
public class DomainException : Exception
{
public DomainException (string message) : base(message) { }
}
+9
View File
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
@@ -1,8 +1,8 @@
using Eshop.Infrastructure.Persistence;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using TemplateWeb.Entities;
namespace TemplateWeb.Data; namespace Eshop.Infrastructure;
public class AppDbContext : IdentityDbContext<UserEntity> public class AppDbContext : IdentityDbContext<UserEntity>
{ {
@@ -1,7 +1,7 @@
using Eshop.Infrastructure.Persistence;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using TemplateWeb.Entities;
namespace TemplateWeb.Data; namespace Eshop.Infrastructure.Data;
public static class DataSeeder public static class DataSeeder
{ {
@@ -0,0 +1,56 @@
using Eshop.Application.Auth;
using Eshop.Infrastructure.Identity;
using Eshop.Infrastructure.Persistence;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Eshop.Infrastructure;
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services) =>
AddApplicationServices(services);
public static IServiceCollection AddInfrastructure(
this IServiceCollection services,
IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(connectionString));
var pwSection = configuration.GetSection("Identity:Password");
services.AddIdentity<UserEntity, IdentityRole>(options =>
{
options.Password.RequiredLength = pwSection.GetValue<int>("RequiredLength", 6);
options.Password.RequireDigit = pwSection.GetValue<bool>("RequireDigit", true);
options.Password.RequireLowercase = pwSection.GetValue<bool>("RequireLowercase", true);
options.Password.RequireUppercase = pwSection.GetValue<bool>("RequireUppercase", true);
options.Password.RequireNonAlphanumeric = pwSection.GetValue<bool>("RequireNonAlphanumeric", false);
options.Password.RequiredUniqueChars = pwSection.GetValue<int>("RequiredUniqueChars", 1);
})
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
services.ConfigureApplicationCookie(options =>
{
options.LoginPath = "/Auth/Login";
options.AccessDeniedPath = "/Auth/AccessDenied";
options.ExpireTimeSpan = TimeSpan.FromHours(2);
});
return AddApplicationServices(services);
}
private static IServiceCollection AddApplicationServices(IServiceCollection services)
{
services.AddScoped<IAuthenticationService, AspNetIdentityAuthenticationService>();
services.AddScoped<IUserAccountService, AspNetIdentityUserAccountService>();
services.AddScoped<IUserAdministrationService, AspNetIdentityUserAdministrationService>();
return services;
}
}
@@ -1,26 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="dotenv.net" Version="4.0.2" /> <FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="..\.dockerignore"> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.10" />
<Link>.dockerignore</Link> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10">
</Content> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Eshop.Application\Eshop.Application.csproj" />
<ProjectReference Include="..\Eshop.Domain\Eshop.Domain.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -0,0 +1,30 @@
using Eshop.Application.Auth;
using Eshop.Infrastructure.Persistence;
using Microsoft.AspNetCore.Identity;
namespace Eshop.Infrastructure.Identity;
public class AspNetIdentityAuthenticationService(
UserManager<UserEntity> users,
SignInManager<UserEntity> signIn) : IAuthenticationService
{
public async Task<SignInResultDto> SignInAsync(
string emailOrUsername,
string password,
bool rememberMe,
CancellationToken cancellationToken)
{
var user = emailOrUsername.Contains('@')
? await users.FindByEmailAsync(emailOrUsername)
: await users.FindByNameAsync(emailOrUsername);
if (user is null)
return SignInResultDto.Failure;
var result = await signIn.PasswordSignInAsync(user, password, rememberMe, lockoutOnFailure: false);
return result.Succeeded ? SignInResultDto.Success : SignInResultDto.Failure;
}
public Task SignOutAsync(CancellationToken cancellationToken) =>
signIn.SignOutAsync();
}
@@ -0,0 +1,112 @@
using Eshop.Application.Auth;
using Eshop.Application.Common;
using Eshop.Infrastructure.Persistence;
using Microsoft.AspNetCore.Identity;
namespace Eshop.Infrastructure.Identity;
public class AspNetIdentityUserAccountService(
UserManager<UserEntity> users,
SignInManager<UserEntity> signIn) : IUserAccountService
{
public async Task<ServiceResult> RegisterAsync(
string userName,
string email,
string password,
CancellationToken cancellationToken)
{
var user = new UserEntity
{
UserName = userName.Trim(),
Email = email.Trim().ToLower(),
};
var result = await users.CreateAsync(user, password);
if (!result.Succeeded)
return ServiceResult.Failure(result.Errors.Select(e => e.Description));
await users.AddToRoleAsync(user, "User");
return ServiceResult.Ok();
}
public async Task<UserProfileDto?> GetProfileAsync(string userId, CancellationToken cancellationToken)
{
var user = await users.FindByIdAsync(userId);
if (user is null) return null;
return new UserProfileDto(user.UserName!, user.Email!);
}
public async Task<ServiceResult> UpdateProfileAsync(
string userId,
UpdateProfileRequest request,
CancellationToken cancellationToken)
{
var user = await users.FindByIdAsync(userId);
if (user is null)
return ServiceResult.Failure("User not found.");
var errors = new List<string>();
if (user.UserName != request.UserName.Trim())
{
var setName = await users.SetUserNameAsync(user, request.UserName.Trim());
if (!setName.Succeeded)
errors.AddRange(setName.Errors.Select(e => e.Description));
}
var normalizedEmail = request.Email.Trim().ToLower();
if (user.Email != normalizedEmail)
{
var setEmail = await users.SetEmailAsync(user, normalizedEmail);
if (!setEmail.Succeeded)
errors.AddRange(setEmail.Errors.Select(e => e.Description));
}
if (!string.IsNullOrWhiteSpace(request.NewPassword))
{
if (string.IsNullOrWhiteSpace(request.CurrentPassword))
return ServiceResult.Failure("Current password is required to set a new password.");
var changePassword = await users.ChangePasswordAsync(
user, request.CurrentPassword, request.NewPassword);
if (!changePassword.Succeeded)
errors.AddRange(changePassword.Errors.Select(e => e.Description));
}
if (errors.Count > 0)
return ServiceResult.Failure(errors);
await signIn.RefreshSignInAsync(user);
return ServiceResult.Ok();
}
public async Task<ServiceResult> DeleteAccountAsync(string userId, CancellationToken cancellationToken)
{
var user = await users.FindByIdAsync(userId);
if (user is null)
return ServiceResult.Failure("User not found.");
await signIn.SignOutAsync();
var result = await users.DeleteAsync(user);
return result.Succeeded
? ServiceResult.Ok()
: ServiceResult.Failure(result.Errors.Select(e => e.Description));
}
public async Task<UserDataExportDto?> ExportUserDataAsync(string userId, CancellationToken cancellationToken)
{
var user = await users.FindByIdAsync(userId);
if (user is null)
return null;
var roles = await users.GetRolesAsync(user);
return new UserDataExportDto(
user.Id,
user.UserName!,
user.Email!,
roles.ToList(),
DateTime.UtcNow);
}
}
@@ -0,0 +1,66 @@
using Eshop.Application.Auth;
using Eshop.Application.Common;
using Eshop.Infrastructure.Persistence;
using Microsoft.AspNetCore.Identity;
namespace Eshop.Infrastructure.Identity;
public class AspNetIdentityUserAdministrationService(
UserManager<UserEntity> users,
RoleManager<IdentityRole> roles) : IUserAdministrationService
{
public async Task<IReadOnlyList<UserWithRolesDto>> GetAllUsersWithRolesAsync(
CancellationToken cancellationToken)
{
var allRoles = roles.Roles.Select(r => r.Name!).ToList();
var viewModels = new List<UserWithRolesDto>();
foreach (var user in users.Users.ToList())
{
var userRoles = await users.GetRolesAsync(user);
viewModels.Add(ToDto(user, userRoles, allRoles));
}
return viewModels;
}
public async Task<UserWithRolesDto?> GetUserWithRolesAsync(
string userId,
CancellationToken cancellationToken)
{
var user = await users.FindByIdAsync(userId);
if (user is null)
return null;
var userRoles = await users.GetRolesAsync(user);
var allRoles = roles.Roles.Select(r => r.Name!).ToList();
return ToDto(user, userRoles, allRoles);
}
public async Task<ServiceResult> UpdateUserRolesAsync(
string userId,
IReadOnlyList<string> selectedRoles,
CancellationToken cancellationToken)
{
var user = await users.FindByIdAsync(userId);
if (user is null)
return ServiceResult.Failure("User not found.");
var currentRoles = await users.GetRolesAsync(user);
await users.RemoveFromRolesAsync(user, currentRoles);
if (selectedRoles.Count <= 0) return ServiceResult.Ok();
var result = await users.AddToRolesAsync(user, selectedRoles);
return !result.Succeeded ? ServiceResult.Failure(result.Errors.Select(e => e.Description)) : ServiceResult.Ok();
}
private static UserWithRolesDto ToDto(
UserEntity user,
IList<string> userRoles,
IList<string> allRoles) =>
new(
new UserDto(user.Id, user.UserName!, user.Email!),
userRoles.ToList(),
allRoles.ToList());
}
@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
namespace TemplateWeb.Entities; namespace Eshop.Infrastructure.Persistence;
public class UserEntity : IdentityUser public class UserEntity : IdentityUser
{ {
@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace TemplateWeb.Areas.Admin.Controllers; namespace Eshop.Web.Areas.Admin.Controllers;
[Area("Admin")] [Area("Admin")]
[Authorize(Roles = "Admin")] [Authorize(Roles = "Admin")]
@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace TemplateWeb.Areas.Admin.Controllers; namespace Eshop.Web.Areas.Admin.Controllers;
public class DashboardController : AdminBaseController public class DashboardController : AdminBaseController
{ {
@@ -0,0 +1,52 @@
using Eshop.Application.Auth;
using Microsoft.AspNetCore.Mvc;
using Eshop.Web.Models;
namespace Eshop.Web.Areas.Admin.Controllers;
public class UsersController : AdminBaseController
{
private readonly IUserAdministrationService _userAdministrationService;
public UsersController(IUserAdministrationService userAdministrationService)
{
_userAdministrationService = userAdministrationService;
}
public async Task<IActionResult> Index()
{
var users = await _userAdministrationService.GetAllUsersWithRolesAsync();
return View(users.Select(ToViewModel));
}
public async Task<IActionResult> Details(string id)
{
var user = await _userAdministrationService.GetUserWithRolesAsync(id);
if (user is null) return NotFound();
return View(ToViewModel(user));
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> UpdateRoles(string id, List<string> selectedRoles)
{
var result = await _userAdministrationService.UpdateUserRolesAsync(id, selectedRoles);
if (!result.Success)
{
TempData["ErrorMessage"] = string.Join(" ", result.Errors);
return RedirectToAction(nameof(Details), new { id });
}
TempData["SuccessMessage"] = "Roles updated successfully.";
return RedirectToAction(nameof(Details), new { id });
}
private static UserWithRolesViewModel ToViewModel(UserWithRolesDto dto) =>
new()
{
User = dto.User,
Roles = dto.Roles,
AllRoles = dto.AllRoles,
};
}
@@ -12,20 +12,25 @@
<div class="alert alert-success">@TempData["SuccessMessage"]</div> <div class="alert alert-success">@TempData["SuccessMessage"]</div>
} }
@if (TempData["ErrorMessage"] != null)
{
<div class="alert alert-danger">@TempData["ErrorMessage"]</div>
}
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header bg-primary text-white"> <div class="card-header bg-primary text-white">
<h3 class="card-title mb-0">User Information: @Model.UserEntity.UserName</h3> <h3 class="card-title mb-0">User Information: @Model.User.UserName</h3>
</div> </div>
<div class="card-body"> <div class="card-body">
<dl class="row"> <dl class="row">
<dt class="col-sm-3">ID</dt> <dt class="col-sm-3">ID</dt>
<dd class="col-sm-9">@Model.UserEntity.Id</dd> <dd class="col-sm-9">@Model.User.Id</dd>
<dt class="col-sm-3">Username</dt> <dt class="col-sm-3">Username</dt>
<dd class="col-sm-9">@Model.UserEntity.UserName</dd> <dd class="col-sm-9">@Model.User.UserName</dd>
<dt class="col-sm-3">Email</dt> <dt class="col-sm-3">Email</dt>
<dd class="col-sm-9">@Model.UserEntity.Email</dd> <dd class="col-sm-9">@Model.User.Email</dd>
<dt class="col-sm-3">Roles</dt> <dt class="col-sm-3">Roles</dt>
<dd class="col-sm-9"> <dd class="col-sm-9">
@@ -43,7 +48,7 @@
<h5 class="mb-0">Edit Roles</h5> <h5 class="mb-0">Edit Roles</h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<form asp-action="UpdateRoles" asp-route-id="@Model.UserEntity.Id" method="post"> <form asp-action="UpdateRoles" asp-route-id="@Model.User.Id" method="post">
@Html.AntiForgeryToken() @Html.AntiForgeryToken()
<div class="d-flex gap-4 flex-wrap mb-3"> <div class="d-flex gap-4 flex-wrap mb-3">
@foreach (var role in Model.AllRoles) @foreach (var role in Model.AllRoles)
@@ -21,9 +21,9 @@
@foreach (var vm in Model) @foreach (var vm in Model)
{ {
<tr> <tr>
<td>@vm.UserEntity.Id</td> <td>@vm.User.Id</td>
<td>@vm.UserEntity.UserName</td> <td>@vm.User.UserName</td>
<td>@vm.UserEntity.Email</td> <td>@vm.User.Email</td>
<td> <td>
@foreach (var role in vm.Roles) @foreach (var role in vm.Roles)
{ {
@@ -31,7 +31,7 @@
} }
</td> </td>
<td> <td>
<a asp-action="Details" asp-route-id="@vm.UserEntity.Id" class="btn btn-sm btn-outline-primary">Details</a> <a asp-action="Details" asp-route-id="@vm.User.Id" class="btn btn-sm btn-outline-primary">Details</a>
</td> </td>
</tr> </tr>
} }
@@ -0,0 +1,4 @@
@using Eshop.Web
@using Eshop.Web.Models
@using Eshop.Application.Auth
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@@ -0,0 +1,206 @@
using System.Security.Claims;
using System.Text;
using System.Text.Json;
using Eshop.Application.Auth;
using Eshop.Web.ViewModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Eshop.Web.Controllers;
public class AuthController : Controller
{
private readonly ILogger<AuthController> _logger;
private readonly IAuthenticationService _authenticationService;
private readonly IUserAccountService _userAccountService;
public AuthController(
ILogger<AuthController> logger,
IAuthenticationService authenticationService,
IUserAccountService userAccountService)
{
_logger = logger;
_authenticationService = authenticationService;
_userAccountService = userAccountService;
}
#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() => View();
[HttpGet]
[Authorize]
public async Task<IActionResult> Profile()
{
var userId = GetCurrentUserId();
if (userId is null) return RedirectToAction("Login");
var profile = await _userAccountService.GetProfileAsync(userId);
if (profile is null) return RedirectToAction("Login");
return View(new ProfileViewModel
{
UserName = profile.UserName,
Email = profile.Email,
});
}
#endregion
#region HttpPost
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(AuthLoginViewModel model)
{
if (!ModelState.IsValid) return View(model);
var result = await _authenticationService.SignInAsync(
model.EmailOrUsername, model.Password, model.RememberMe);
if (!result.Succeeded)
{
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]
public async Task<IActionResult> Register(AuthRegisterViewModel model)
{
if (!ModelState.IsValid) return View(model);
var result = await _userAccountService.RegisterAsync(
model.UserName, model.Email, model.Password);
if (!result.Success)
{
foreach (var error in result.Errors)
ModelState.AddModelError(string.Empty, error);
return View(model);
}
_logger.LogInformation("User {UserName} registered.", model.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 userId = GetCurrentUserId();
if (userId is null) return RedirectToAction("Login");
var result = await _userAccountService.UpdateProfileAsync(
userId,
new UpdateProfileRequest(
model.UserName,
model.Email,
model.CurrentPassword,
model.NewPassword));
if (!result.Success)
{
foreach (var error in result.Errors)
ModelState.AddModelError(string.Empty, error);
return View(model);
}
TempData["SuccessMessage"] = "Profile updated successfully.";
return RedirectToAction("Profile");
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout()
{
await _authenticationService.SignOutAsync();
return RedirectToAction("Index", "Home");
}
[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteAccount()
{
var userId = GetCurrentUserId();
if (userId is null) return RedirectToAction("Login");
var result = await _userAccountService.DeleteAccountAsync(userId);
if (!result.Success)
{
foreach (var error in result.Errors)
ModelState.AddModelError(string.Empty, error);
return RedirectToAction("Profile");
}
_logger.LogInformation("User {UserId} deleted their account.", userId);
return RedirectToAction("Register");
}
[HttpGet]
[Authorize]
public async Task<IActionResult> DownloadData()
{
var userId = GetCurrentUserId();
if (userId is null) return RedirectToAction("Login");
var data = await _userAccountService.ExportUserDataAsync(userId);
if (data is null) return RedirectToAction("Login");
var export = new
{
data.Id,
data.UserName,
data.Email,
Roles = data.Roles,
data.ExportDate,
};
var json = JsonSerializer.Serialize(export, new JsonSerializerOptions { WriteIndented = true });
var bytes = Encoding.UTF8.GetBytes(json);
return File(bytes, "application/json", $"user_data_{data.UserName}.json");
}
#endregion
private string? GetCurrentUserId() =>
User.FindFirstValue(ClaimTypes.NameIdentifier);
}
@@ -1,8 +1,8 @@
using System.Diagnostics; using System.Diagnostics;
using Eshop.Web.ViewModels;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using TemplateWeb.Models;
namespace TemplateWeb.Controllers; namespace Eshop.Web.Controllers;
public class HomeController : Controller public class HomeController : Controller
{ {
+25
View File
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="dotenv.net" Version="4.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Eshop.Application\Eshop.Application.csproj" />
<ProjectReference Include="..\Eshop.Infrastructure\Eshop.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
</ItemGroup>
</Project>
@@ -0,0 +1,10 @@
using Eshop.Application.Auth;
namespace Eshop.Web.Models;
public class UserWithRolesViewModel
{
public UserDto User { get; set; } = null!;
public IReadOnlyList<string> Roles { get; set; } = [];
public IReadOnlyList<string> AllRoles { get; set; } = [];
}
+52
View File
@@ -0,0 +1,52 @@
using dotenv.net;
using Eshop.Infrastructure;
using Eshop.Infrastructure.Data;
using Eshop.Infrastructure.Persistence;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
namespace Eshop.Web;
public class Program
{
public static void Main(string[] args)
{
DotEnv.Load();
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddInfrastructure(builder.Configuration);
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
if (db.Database.GetPendingMigrations().Any())
db.Database.Migrate();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<UserEntity>>();
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
DataSeeder.SeedAsync(userManager, roleManager).GetAwaiter().GetResult();
}
if (!app.Environment.IsDevelopment())
app.UseExceptionHandler("/Home/Error");
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapStaticAssets();
app.MapControllerRoute(
name: "areas",
pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}")
.WithStaticAssets();
app.Run();
}
}
@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace TemplateWeb.Models; namespace Eshop.Web.ViewModels;
public class AuthLoginViewModel public class AuthLoginViewModel
{ {
@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace TemplateWeb.Models; namespace Eshop.Web.ViewModels;
public class AuthRegisterViewModel public class AuthRegisterViewModel
{ {
@@ -1,4 +1,4 @@
namespace TemplateWeb.Models; namespace Eshop.Web.ViewModels;
public class ErrorViewModel public class ErrorViewModel
{ {
@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace TemplateWeb.Models; namespace Eshop.Web.ViewModels;
public class ProfileViewModel public class ProfileViewModel
{ {
@@ -1,4 +1,4 @@
@model AuthLoginViewModel @model Eshop.Web.ViewModels.AuthLoginViewModel
<div class="row justify-content-center mt-5"> <div class="row justify-content-center mt-5">
<div class="col-md-4"> <div class="col-md-4">
@@ -1,4 +1,4 @@
@model ProfileViewModel @model Eshop.Web.ViewModels.ProfileViewModel
@{ @{
ViewData["Title"] = "My Profile"; ViewData["Title"] = "My Profile";
} }
@@ -1,4 +1,4 @@
@model AuthRegisterViewModel @model Eshop.Web.ViewModels.AuthRegisterViewModel
<div class="row justify-content-center mt-5"> <div class="row justify-content-center mt-5">
<div class="col-md-4"> <div class="col-md-4">
@@ -1,4 +1,4 @@
@model ErrorViewModel @model Eshop.Web.ViewModels.ErrorViewModel
@{ @{
ViewData["Title"] = "Error"; ViewData["Title"] = "Error";
} }
@@ -1,3 +1,3 @@
@using TemplateWeb @using Eshop.Web
@using TemplateWeb.Models @using Eshop.Web.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Some files were not shown because too many files have changed in this diff Show More