generated from egmont11/ASP.Net-Core-MVC-Template
InitEshop+ddd+ca
This commit is contained in:
Generated
+15
@@ -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
|
||||
Generated
Generated
Generated
+6
@@ -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>
|
||||
Generated
+4
@@ -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>
|
||||
@@ -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) { }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
using Eshop.Infrastructure.Persistence;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TemplateWeb.Entities;
|
||||
|
||||
namespace TemplateWeb.Data;
|
||||
namespace Eshop.Infrastructure;
|
||||
|
||||
public class AppDbContext : IdentityDbContext<UserEntity>
|
||||
{
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
using Eshop.Infrastructure.Persistence;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using TemplateWeb.Entities;
|
||||
|
||||
namespace TemplateWeb.Data;
|
||||
namespace Eshop.Infrastructure.Data;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
+14
-13
@@ -1,26 +1,27 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="dotenv.net" Version="4.0.2" />
|
||||
<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" />
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\.dockerignore">
|
||||
<Link>.dockerignore</Link>
|
||||
</Content>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10">
|
||||
<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>
|
||||
|
||||
</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
-1
@@ -1,6 +1,6 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace TemplateWeb.Entities;
|
||||
namespace Eshop.Infrastructure.Persistence;
|
||||
|
||||
public class UserEntity : IdentityUser
|
||||
{
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace TemplateWeb.Areas.Admin.Controllers;
|
||||
namespace Eshop.Web.Areas.Admin.Controllers;
|
||||
|
||||
[Area("Admin")]
|
||||
[Authorize(Roles = "Admin")]
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace TemplateWeb.Areas.Admin.Controllers;
|
||||
namespace Eshop.Web.Areas.Admin.Controllers;
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
+10
-5
@@ -12,20 +12,25 @@
|
||||
<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-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 class="card-body">
|
||||
<dl class="row">
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<dd class="col-sm-9">
|
||||
@@ -43,7 +48,7 @@
|
||||
<h5 class="mb-0">Edit Roles</h5>
|
||||
</div>
|
||||
<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()
|
||||
<div class="d-flex gap-4 flex-wrap mb-3">
|
||||
@foreach (var role in Model.AllRoles)
|
||||
+4
-4
@@ -21,9 +21,9 @@
|
||||
@foreach (var vm in Model)
|
||||
{
|
||||
<tr>
|
||||
<td>@vm.UserEntity.Id</td>
|
||||
<td>@vm.UserEntity.UserName</td>
|
||||
<td>@vm.UserEntity.Email</td>
|
||||
<td>@vm.User.Id</td>
|
||||
<td>@vm.User.UserName</td>
|
||||
<td>@vm.User.Email</td>
|
||||
<td>
|
||||
@foreach (var role in vm.Roles)
|
||||
{
|
||||
@@ -31,7 +31,7 @@
|
||||
}
|
||||
</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>
|
||||
</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);
|
||||
}
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
using System.Diagnostics;
|
||||
using Eshop.Web.ViewModels;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TemplateWeb.Models;
|
||||
|
||||
namespace TemplateWeb.Controllers;
|
||||
namespace Eshop.Web.Controllers;
|
||||
|
||||
public class HomeController : Controller
|
||||
{
|
||||
@@ -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; } = [];
|
||||
}
|
||||
@@ -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
-1
@@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TemplateWeb.Models;
|
||||
namespace Eshop.Web.ViewModels;
|
||||
|
||||
public class AuthLoginViewModel
|
||||
{
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TemplateWeb.Models;
|
||||
namespace Eshop.Web.ViewModels;
|
||||
|
||||
public class AuthRegisterViewModel
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace TemplateWeb.Models;
|
||||
namespace Eshop.Web.ViewModels;
|
||||
|
||||
public class ErrorViewModel
|
||||
{
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TemplateWeb.Models;
|
||||
namespace Eshop.Web.ViewModels;
|
||||
|
||||
public class ProfileViewModel
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
@model AuthLoginViewModel
|
||||
@model Eshop.Web.ViewModels.AuthLoginViewModel
|
||||
|
||||
<div class="row justify-content-center mt-5">
|
||||
<div class="col-md-4">
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
@model ProfileViewModel
|
||||
@model Eshop.Web.ViewModels.ProfileViewModel
|
||||
@{
|
||||
ViewData["Title"] = "My Profile";
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
@model AuthRegisterViewModel
|
||||
@model Eshop.Web.ViewModels.AuthRegisterViewModel
|
||||
|
||||
<div class="row justify-content-center mt-5">
|
||||
<div class="col-md-4">
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
@model ErrorViewModel
|
||||
@model Eshop.Web.ViewModels.ErrorViewModel
|
||||
@{
|
||||
ViewData["Title"] = "Error";
|
||||
}
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
@using TemplateWeb
|
||||
@using TemplateWeb.Models
|
||||
@using Eshop.Web
|
||||
@using Eshop.Web.Models
|
||||
@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
Reference in New Issue
Block a user