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
+25
View File
@@ -0,0 +1,25 @@
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/.idea
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
+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
+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>
+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>
+15
View File
@@ -0,0 +1,15 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/contentModel.xml
/projectSettingsUpdater.xml
/modules.xml
/.idea.Template.iml
# Editor-based HTTP Client requests
/httpRequests/
# Ignored default folder with query files
/queries/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+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>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</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>
@@ -0,0 +1,13 @@
using Eshop.Infrastructure.Persistence;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace Eshop.Infrastructure;
public class AppDbContext : IdentityDbContext<UserEntity>
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options)
{
}
}
@@ -0,0 +1,33 @@
using Eshop.Infrastructure.Persistence;
using Microsoft.AspNetCore.Identity;
namespace Eshop.Infrastructure.Data;
public static class DataSeeder
{
public static async Task SeedAsync(UserManager<UserEntity> userManager, RoleManager<IdentityRole> roleManager)
{
// Ensure roles exist
foreach (var role in new[] { "Admin", "User" })
{
if (!await roleManager.RoleExistsAsync(role))
await roleManager.CreateAsync(new IdentityRole(role));
}
// Seed initial admin user if none exists
if (!userManager.Users.Any())
{
var admin = new UserEntity
{
UserName = "admin",
Email = "admin@template.com",
EmailConfirmed = true,
};
// You should change this password on first login
var result = await userManager.CreateAsync(admin, "Admin123!");
if (result.Succeeded)
await userManager.AddToRoleAsync(admin, "Admin");
}
}
}
@@ -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;
}
}
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<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());
}
@@ -0,0 +1,7 @@
using Microsoft.AspNetCore.Identity;
namespace Eshop.Infrastructure.Persistence;
public class UserEntity : IdentityUser
{
}
@@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Eshop.Web.Areas.Admin.Controllers;
[Area("Admin")]
[Authorize(Roles = "Admin")]
public abstract class AdminBaseController : Controller
{
}
@@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Mvc;
namespace Eshop.Web.Areas.Admin.Controllers;
public class DashboardController : AdminBaseController
{
public IActionResult Index()
{
return View();
}
}
@@ -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,
};
}
@@ -0,0 +1,19 @@
@{
ViewData["Title"] = "Dashboard";
}
<h1>Admin Dashboard</h1>
<p>Welcome to the administration area.</p>
<div class="row mt-4">
<div class="col-md-4">
<div class="card text-white bg-primary mb-3">
<div class="card-header">Users</div>
<div class="card-body">
<h5 class="card-title">Manage system users</h5>
<p class="card-text">View and edit user details.</p>
<a asp-controller="Users" asp-action="Index" class="btn btn-light">Go to Users</a>
</div>
</div>
</div>
</div>
@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>@ViewData["Title"] - Admin Panel</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css"/>
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true"/>
<style>
body { display: flex; min-height: 100vh; flex-direction: column; }
.wrapper { display: flex; flex: 1; }
#sidebar { min-width: 250px; max-width: 250px; background: #343a40; color: #fff; transition: all 0.3s; }
#sidebar .sidebar-header { padding: 20px; background: #3c444b; }
#sidebar ul.components { padding: 20px 0; border-bottom: 1px solid #47748b; }
#sidebar ul p { color: #fff; padding: 10px; }
#sidebar ul li a { padding: 10px; font-size: 1.1em; display: block; color: #fff; text-decoration: none; }
#sidebar ul li a:hover { color: #343a40; background: #fff; }
#content { width: 100%; padding: 20px; }
</style>
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-dark bg-dark border-bottom box-shadow">
<div class="container-fluid">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">TemplateWeb <small class="text-muted">Admin</small></a>
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<ul class="navbar-nav flex-grow-1">
</ul>
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link text-light" asp-area="" asp-controller="Home" asp-action="Index">Back to Site</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="wrapper">
<!-- Sidebar -->
<nav id="sidebar">
<div class="sidebar-header">
<h3>Admin Panel</h3>
</div>
<ul class="list-unstyled components">
<li class="@(ViewContext.RouteData.Values["controller"]?.ToString() == "Dashboard" ? "active" : "")">
<a asp-area="Admin" asp-controller="Dashboard" asp-action="Index">Dashboard</a>
</li>
<li class="@(ViewContext.RouteData.Values["controller"]?.ToString() == "Users" ? "active" : "")">
<a asp-area="Admin" asp-controller="Users" asp-action="Index">Users</a>
</li>
<!-- Add more models here easily -->
</ul>
</nav>
<!-- Page Content -->
<div id="content">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
</div>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
@@ -0,0 +1,67 @@
@model UserWithRolesViewModel
@{
ViewData["Title"] = "User Details";
}
<div class="mb-4">
<a asp-action="Index" class="btn btn-secondary">&larr; Back to List</a>
</div>
@if (TempData["SuccessMessage"] != null)
{
<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.User.UserName</h3>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-3">ID</dt>
<dd class="col-sm-9">@Model.User.Id</dd>
<dt class="col-sm-3">Username</dt>
<dd class="col-sm-9">@Model.User.UserName</dd>
<dt class="col-sm-3">Email</dt>
<dd class="col-sm-9">@Model.User.Email</dd>
<dt class="col-sm-3">Roles</dt>
<dd class="col-sm-9">
@foreach (var role in Model.Roles)
{
<span class="badge @(role == "Admin" ? "bg-danger" : "bg-info")">@role</span>
}
</dd>
</dl>
</div>
</div>
<div class="card">
<div class="card-header">
<h5 class="mb-0">Edit Roles</h5>
</div>
<div class="card-body">
<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)
{
<div class="form-check">
<input class="form-check-input" type="checkbox"
name="selectedRoles" value="@role" id="role_@role"
@(Model.Roles.Contains(role) ? "checked" : "") />
<label class="form-check-label" for="role_@role">@role</label>
</div>
}
</div>
<button type="submit" class="btn btn-primary">Save Roles</button>
</form>
</div>
</div>
@@ -0,0 +1,39 @@
@model IEnumerable<UserWithRolesViewModel>
@{
ViewData["Title"] = "User Management";
}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>Users</h1>
</div>
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>ID</th>
<th>Username</th>
<th>Email</th>
<th>Roles</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@foreach (var vm in Model)
{
<tr>
<td>@vm.User.Id</td>
<td>@vm.User.UserName</td>
<td>@vm.User.Email</td>
<td>
@foreach (var role in vm.Roles)
{
<span class="badge @(role == "Admin" ? "bg-danger" : "bg-info")">@role</span>
}
</td>
<td>
<a asp-action="Details" asp-route-id="@vm.User.Id" class="btn btn-sm btn-outline-primary">Details</a>
</td>
</tr>
}
</tbody>
</table>
@@ -0,0 +1,4 @@
@using Eshop.Web
@using Eshop.Web.Models
@using Eshop.Application.Auth
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@@ -0,0 +1,3 @@
@{
Layout = "_AdminLayout";
}
@@ -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);
}
@@ -0,0 +1,24 @@
using System.Diagnostics;
using Eshop.Web.ViewModels;
using Microsoft.AspNetCore.Mvc;
namespace Eshop.Web.Controllers;
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
public IActionResult Privacy()
{
return View();
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
+23
View File
@@ -0,0 +1,23 @@
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
USER $APP_UID
WORKDIR /app
EXPOSE 8080
EXPOSE 8081
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["TemplateWeb/TemplateWeb.csproj", "TemplateWeb/"]
RUN dotnet restore "TemplateWeb/TemplateWeb.csproj"
COPY . .
WORKDIR "/src/TemplateWeb"
RUN dotnet build "./TemplateWeb.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./TemplateWeb.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "TemplateWeb.dll"]
+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();
}
}
@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5240",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
@@ -0,0 +1,19 @@
using System.ComponentModel.DataAnnotations;
namespace Eshop.Web.ViewModels;
public class AuthLoginViewModel
{
[Required(ErrorMessage = "Email or username is required")]
[Display(Name = "Email or Username")]
public string EmailOrUsername { get; set; } = "";
[Display(Name = "Password")]
[DataType(DataType.Password)]
[Required] [MinLength(4)]
public string Password { get; set; }
[Display(Name = "Remember me?")]
public bool RememberMe { get; set; } = false;
public string? ReturnUrl { get; set; }
}
@@ -0,0 +1,21 @@
using System.ComponentModel.DataAnnotations;
namespace Eshop.Web.ViewModels;
public class AuthRegisterViewModel
{
[Required] [Display(Name = "Username")] [StringLength(50, MinimumLength = 3)]
public string UserName { get; set; }
[Required] [EmailAddress] [Display(Name = "Email")]
public string Email { get; set; }
[Required] [DataType(DataType.Password)] [Display(Name = "Password")]
public string Password { get; set; }
[Display(Name = "Confirm password")]
[Required] [DataType(DataType.Password)] [Compare("Password")]
public string ConfirmPassword { get; set; }
public string? ReturnUrl { get; set; }
}
@@ -0,0 +1,8 @@
namespace Eshop.Web.ViewModels;
public class ErrorViewModel
{
public string? RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
}
@@ -0,0 +1,29 @@
using System.ComponentModel.DataAnnotations;
namespace Eshop.Web.ViewModels;
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 = "Current Password")]
public string? CurrentPassword { get; set; }
[DataType(DataType.Password)]
[Display(Name = "New Password (leave blank to keep current)")]
public string? NewPassword { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm New Password")]
[Compare("NewPassword")]
public string? ConfirmPassword { get; set; }
}
@@ -0,0 +1,2 @@
<h1>Access Denied!</h1>
+43
View File
@@ -0,0 +1,43 @@
@model Eshop.Web.ViewModels.AuthLoginViewModel
<div class="row justify-content-center mt-5">
<div class="col-md-4">
<div class="card shadow-sm">
<div class="card-body p-4">
<h1 class="card-title text-center mb-4">Login</h1>
<form asp-action="Login">
@Html.AntiForgeryToken()
<div asp-validation-summary="ModelOnly" class="alert alert-danger" role="alert"></div>
<div class="mb-3">
<label asp-for="EmailOrUsername"></label>
<input asp-for="EmailOrUsername" class="form-control" placeholder="Email or Username"/>
<span asp-validation-for="EmailOrUsername" class="text-danger small"></span>
</div>
<div class="mb-3">
<label asp-for="Password"></label>
<input asp-for="Password" class="form-control" placeholder="Password"/>
<span asp-validation-for="Password" class="text-danger small"></span>
</div>
<div class="mb-3">
<label asp-for="RememberMe"></label>
<input asp-for="RememberMe" class="form-check-input"/>
</div>
<input asp-for="ReturnUrl" type="hidden"/>
<div class="d-grid">
<input type="submit" value="Login" class="btn btn-primary"/>
</div>
</form>
</div>
</div>
</div>
</div>
@section Scripts {
@{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
}
+82
View File
@@ -0,0 +1,82 @@
@model Eshop.Web.ViewModels.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="CurrentPassword" class="form-label"></label>
<input asp-for="CurrentPassword" class="form-control" />
<span asp-validation-for="CurrentPassword" class="text-danger small"></span>
</div>
<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"); }
}
@@ -0,0 +1,50 @@
@model Eshop.Web.ViewModels.AuthRegisterViewModel
<div class="row justify-content-center mt-5">
<div class="col-md-4">
<div class="card shadow-sm">
<div class="card-body p-4">
<h1 class="card-title text-center mb-4">Register</h1>
<form asp-action="Register">
@Html.AntiForgeryToken()
<div asp-validation-summary="ModelOnly" class="alert alert-danger" role="alert"></div>
<div class="mb-3">
<label asp-for="UserName" class="form-label"></label>
<input asp-for="UserName" class="form-control" placeholder="Username"/>
<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" placeholder="Email"/>
<span asp-validation-for="Email" class="text-danger small"></span>
</div>
<div class="mb-3">
<label asp-for="Password" class="form-label"></label>
<input asp-for="Password" class="form-control" placeholder="Password"/>
<span asp-validation-for="Password" 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" placeholder="Confirm Password"/>
<span asp-validation-for="ConfirmPassword" class="text-danger small"></span>
</div>
<input asp-for="ReturnUrl" type="hidden"/>
<div class="d-grid">
<input type="submit" value="Register" class="btn btn-primary"/>
</div>
</form>
</div>
</div>
</div>
</div>
@section Scripts {
@{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
}
+8
View File
@@ -0,0 +1,8 @@
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>
@@ -0,0 +1,6 @@
@{
ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>
<p>Use this page to detail your site's privacy policy.</p>
+25
View File
@@ -0,0 +1,25 @@
@model Eshop.Web.ViewModels.ErrorViewModel
@{
ViewData["Title"] = "Error";
}
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (Model.ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@Model.RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>@ViewData["Title"] - TemplateWeb</title>
<script type="importmap"></script>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css"/>
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true"/>
<link rel="stylesheet" href="~/TemplateWeb.styles.css" asp-append-version="true"/>
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container-fluid">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">TemplateWeb</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</li>
</ul>
<ul class="navbar-nav">
@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"))
{
<li><a class="dropdown-item text-primary fw-bold" asp-area="Admin" asp-controller="Dashboard" asp-action="Index">Admin Panel</a></li>
<li><hr class="dropdown-divider"></li>
}
<li><a class="dropdown-item" asp-area="" asp-controller="Auth" asp-action="Profile">My Profile</a></li>
<li><hr class="dropdown-divider"></li>
<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>
}
else
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Auth" asp-action="Register">Register</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Auth" asp-action="Login">Login</a>
</li>
}
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
<footer class="border-top footer text-muted">
<div class="container">
&copy; 2026 - TemplateWeb - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</div>
</footer>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
@@ -0,0 +1,48 @@
/* Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification
for details on configuring this project to bundle and minify static web assets. */
a.navbar-brand {
white-space: normal;
text-align: center;
word-break: break-all;
}
a {
color: #0077cc;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.nav-pills .nav-link.active, .nav-pills .show > .nav-link {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.border-top {
border-top: 1px solid #e5e5e5;
}
.border-bottom {
border-bottom: 1px solid #e5e5e5;
}
.box-shadow {
box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
}
button.accept-policy {
font-size: 1rem;
line-height: inherit;
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
white-space: nowrap;
line-height: 60px;
}
@@ -0,0 +1,2 @@
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/dist/jquery.validate.unobtrusive.min.js"></script>
@@ -0,0 +1,3 @@
@using Eshop.Web
@using Eshop.Web.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
+3
View File
@@ -0,0 +1,3 @@
@{
Layout = "_Layout";
}
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft.AspNetCore": "Warning"
}
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Database=templatedb;Username=user;Password=password"
},
"Identity": {
"Password": {
"RequiredLength": 6,
"RequireDigit": true,
"RequireLowercase": true,
"RequireUppercase": true,
"RequireNonAlphanumeric": false,
"RequiredUniqueChars": 1
}
}
}
+31
View File
@@ -0,0 +1,31 @@
html {
font-size: 14px;
}
@media (min-width: 768px) {
html {
font-size: 16px;
}
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
html {
position: relative;
min-height: 100%;
}
body {
margin-bottom: 60px;
}
.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
color: var(--bs-secondary-color);
text-align: end;
}
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
text-align: start;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

+4
View File
@@ -0,0 +1,4 @@
// Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification
// for details on configuring this project to bundle and minify static web assets.
// Write your JavaScript code.
@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2011-2021 Twitter, Inc.
Copyright (c) 2011-2021 The Bootstrap Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,597 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,594 @@
/*!
* Bootstrap Reboot v5.3.3 (https://getbootstrap.com/)
* Copyright 2011-2024 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
:root,
[data-bs-theme=light] {
--bs-blue: #0d6efd;
--bs-indigo: #6610f2;
--bs-purple: #6f42c1;
--bs-pink: #d63384;
--bs-red: #dc3545;
--bs-orange: #fd7e14;
--bs-yellow: #ffc107;
--bs-green: #198754;
--bs-teal: #20c997;
--bs-cyan: #0dcaf0;
--bs-black: #000;
--bs-white: #fff;
--bs-gray: #6c757d;
--bs-gray-dark: #343a40;
--bs-gray-100: #f8f9fa;
--bs-gray-200: #e9ecef;
--bs-gray-300: #dee2e6;
--bs-gray-400: #ced4da;
--bs-gray-500: #adb5bd;
--bs-gray-600: #6c757d;
--bs-gray-700: #495057;
--bs-gray-800: #343a40;
--bs-gray-900: #212529;
--bs-primary: #0d6efd;
--bs-secondary: #6c757d;
--bs-success: #198754;
--bs-info: #0dcaf0;
--bs-warning: #ffc107;
--bs-danger: #dc3545;
--bs-light: #f8f9fa;
--bs-dark: #212529;
--bs-primary-rgb: 13, 110, 253;
--bs-secondary-rgb: 108, 117, 125;
--bs-success-rgb: 25, 135, 84;
--bs-info-rgb: 13, 202, 240;
--bs-warning-rgb: 255, 193, 7;
--bs-danger-rgb: 220, 53, 69;
--bs-light-rgb: 248, 249, 250;
--bs-dark-rgb: 33, 37, 41;
--bs-primary-text-emphasis: #052c65;
--bs-secondary-text-emphasis: #2b2f32;
--bs-success-text-emphasis: #0a3622;
--bs-info-text-emphasis: #055160;
--bs-warning-text-emphasis: #664d03;
--bs-danger-text-emphasis: #58151c;
--bs-light-text-emphasis: #495057;
--bs-dark-text-emphasis: #495057;
--bs-primary-bg-subtle: #cfe2ff;
--bs-secondary-bg-subtle: #e2e3e5;
--bs-success-bg-subtle: #d1e7dd;
--bs-info-bg-subtle: #cff4fc;
--bs-warning-bg-subtle: #fff3cd;
--bs-danger-bg-subtle: #f8d7da;
--bs-light-bg-subtle: #fcfcfd;
--bs-dark-bg-subtle: #ced4da;
--bs-primary-border-subtle: #9ec5fe;
--bs-secondary-border-subtle: #c4c8cb;
--bs-success-border-subtle: #a3cfbb;
--bs-info-border-subtle: #9eeaf9;
--bs-warning-border-subtle: #ffe69c;
--bs-danger-border-subtle: #f1aeb5;
--bs-light-border-subtle: #e9ecef;
--bs-dark-border-subtle: #adb5bd;
--bs-white-rgb: 255, 255, 255;
--bs-black-rgb: 0, 0, 0;
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
--bs-body-font-family: var(--bs-font-sans-serif);
--bs-body-font-size: 1rem;
--bs-body-font-weight: 400;
--bs-body-line-height: 1.5;
--bs-body-color: #212529;
--bs-body-color-rgb: 33, 37, 41;
--bs-body-bg: #fff;
--bs-body-bg-rgb: 255, 255, 255;
--bs-emphasis-color: #000;
--bs-emphasis-color-rgb: 0, 0, 0;
--bs-secondary-color: rgba(33, 37, 41, 0.75);
--bs-secondary-color-rgb: 33, 37, 41;
--bs-secondary-bg: #e9ecef;
--bs-secondary-bg-rgb: 233, 236, 239;
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
--bs-tertiary-color-rgb: 33, 37, 41;
--bs-tertiary-bg: #f8f9fa;
--bs-tertiary-bg-rgb: 248, 249, 250;
--bs-heading-color: inherit;
--bs-link-color: #0d6efd;
--bs-link-color-rgb: 13, 110, 253;
--bs-link-decoration: underline;
--bs-link-hover-color: #0a58ca;
--bs-link-hover-color-rgb: 10, 88, 202;
--bs-code-color: #d63384;
--bs-highlight-color: #212529;
--bs-highlight-bg: #fff3cd;
--bs-border-width: 1px;
--bs-border-style: solid;
--bs-border-color: #dee2e6;
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
--bs-border-radius: 0.375rem;
--bs-border-radius-sm: 0.25rem;
--bs-border-radius-lg: 0.5rem;
--bs-border-radius-xl: 1rem;
--bs-border-radius-xxl: 2rem;
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
--bs-border-radius-pill: 50rem;
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
--bs-focus-ring-width: 0.25rem;
--bs-focus-ring-opacity: 0.25;
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
--bs-form-valid-color: #198754;
--bs-form-valid-border-color: #198754;
--bs-form-invalid-color: #dc3545;
--bs-form-invalid-border-color: #dc3545;
}
[data-bs-theme=dark] {
color-scheme: dark;
--bs-body-color: #dee2e6;
--bs-body-color-rgb: 222, 226, 230;
--bs-body-bg: #212529;
--bs-body-bg-rgb: 33, 37, 41;
--bs-emphasis-color: #fff;
--bs-emphasis-color-rgb: 255, 255, 255;
--bs-secondary-color: rgba(222, 226, 230, 0.75);
--bs-secondary-color-rgb: 222, 226, 230;
--bs-secondary-bg: #343a40;
--bs-secondary-bg-rgb: 52, 58, 64;
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
--bs-tertiary-color-rgb: 222, 226, 230;
--bs-tertiary-bg: #2b3035;
--bs-tertiary-bg-rgb: 43, 48, 53;
--bs-primary-text-emphasis: #6ea8fe;
--bs-secondary-text-emphasis: #a7acb1;
--bs-success-text-emphasis: #75b798;
--bs-info-text-emphasis: #6edff6;
--bs-warning-text-emphasis: #ffda6a;
--bs-danger-text-emphasis: #ea868f;
--bs-light-text-emphasis: #f8f9fa;
--bs-dark-text-emphasis: #dee2e6;
--bs-primary-bg-subtle: #031633;
--bs-secondary-bg-subtle: #161719;
--bs-success-bg-subtle: #051b11;
--bs-info-bg-subtle: #032830;
--bs-warning-bg-subtle: #332701;
--bs-danger-bg-subtle: #2c0b0e;
--bs-light-bg-subtle: #343a40;
--bs-dark-bg-subtle: #1a1d20;
--bs-primary-border-subtle: #084298;
--bs-secondary-border-subtle: #41464b;
--bs-success-border-subtle: #0f5132;
--bs-info-border-subtle: #087990;
--bs-warning-border-subtle: #997404;
--bs-danger-border-subtle: #842029;
--bs-light-border-subtle: #495057;
--bs-dark-border-subtle: #343a40;
--bs-heading-color: inherit;
--bs-link-color: #6ea8fe;
--bs-link-hover-color: #8bb9fe;
--bs-link-color-rgb: 110, 168, 254;
--bs-link-hover-color-rgb: 139, 185, 254;
--bs-code-color: #e685b5;
--bs-highlight-color: #dee2e6;
--bs-highlight-bg: #664d03;
--bs-border-color: #495057;
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
--bs-form-valid-color: #75b798;
--bs-form-valid-border-color: #75b798;
--bs-form-invalid-color: #ea868f;
--bs-form-invalid-border-color: #ea868f;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: var(--bs-body-font-family);
font-size: var(--bs-body-font-size);
font-weight: var(--bs-body-font-weight);
line-height: var(--bs-body-line-height);
color: var(--bs-body-color);
text-align: var(--bs-body-text-align);
background-color: var(--bs-body-bg);
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
border: 0;
border-top: var(--bs-border-width) solid;
opacity: 0.25;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
color: var(--bs-heading-color);
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-right: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.1875em;
color: var(--bs-highlight-color);
background-color: var(--bs-highlight-bg);
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
text-decoration: underline;
}
a:hover {
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: var(--bs-font-monospace);
font-size: 1em;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: var(--bs-code-color);
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.1875rem 0.375rem;
font-size: 0.875em;
color: var(--bs-body-bg);
background-color: var(--bs-body-color);
border-radius: 0.25rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: var(--bs-secondary-color);
text-align: right;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
display: none !important;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
::file-selector-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

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