Shady nagy/net6 (#614)

* udated to .net6

* used the .net6 version RC2

* added editconfig.

* App core new Scoped Namespaces style.

* BlazorAdmin new Scoped Namespaces style.

* Blazor Shared new Scoped Namespaces style.

* Infra new Scoped Namespaces style.

* public api new Scoped Namespaces style.

* web new Scoped Namespaces style.

* FunctionalTests new Scoped Namespaces style.

* Integrational tests new Scoped Namespaces style.

* unit tests new Scoped Namespaces style.

* update github action.

* update github action.

* change the global.
This commit is contained in:
Shady Nagy
2021-11-06 01:55:48 +02:00
committed by GitHub
parent 64f150dc07
commit 9db2feb930
252 changed files with 6307 additions and 6413 deletions

View File

@@ -1,14 +1,14 @@
using Microsoft.AspNetCore.Hosting;
[assembly: HostingStartup(typeof(Microsoft.eShopWeb.Web.Areas.Identity.IdentityHostingStartup))]
namespace Microsoft.eShopWeb.Web.Areas.Identity
namespace Microsoft.eShopWeb.Web.Areas.Identity;
public class IdentityHostingStartup : IHostingStartup
{
public class IdentityHostingStartup : IHostingStartup
public void Configure(IWebHostBuilder builder)
{
public void Configure(IWebHostBuilder builder)
builder.ConfigureServices((context, services) =>
{
builder.ConfigureServices((context, services) => {
});
}
});
}
}
}

View File

@@ -8,38 +8,37 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.eShopWeb.Infrastructure.Identity;
namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account
namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account;
[AllowAnonymous]
public class ConfirmEmailModel : PageModel
{
[AllowAnonymous]
public class ConfirmEmailModel : PageModel
private readonly UserManager<ApplicationUser> _userManager;
public ConfirmEmailModel(UserManager<ApplicationUser> userManager)
{
private readonly UserManager<ApplicationUser> _userManager;
_userManager = userManager;
}
public ConfirmEmailModel(UserManager<ApplicationUser> userManager)
public async Task<IActionResult> OnGetAsync(string userId, string code)
{
if (userId == null || code == null)
{
_userManager = userManager;
return RedirectToPage("/Index");
}
public async Task<IActionResult> OnGetAsync(string userId, string code)
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
if (userId == null || code == null)
{
return RedirectToPage("/Index");
}
var user = await _userManager.FindByIdAsync(userId);
if (user == null)
{
return NotFound($"Unable to load user with ID '{userId}'.");
}
var result = await _userManager.ConfirmEmailAsync(user, code);
if (!result.Succeeded)
{
throw new InvalidOperationException($"Error confirming email for user with ID '{userId}':");
}
return Page();
return NotFound($"Unable to load user with ID '{userId}'.");
}
var result = await _userManager.ConfirmEmailAsync(user, code);
if (!result.Succeeded)
{
throw new InvalidOperationException($"Error confirming email for user with ID '{userId}':");
}
return Page();
}
}

View File

@@ -1,4 +1,9 @@
using Microsoft.AspNetCore.Authentication;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
@@ -7,117 +12,111 @@ using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Microsoft.eShopWeb.Infrastructure.Identity;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account
namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account;
[AllowAnonymous]
public class LoginModel : PageModel
{
[AllowAnonymous]
public class LoginModel : PageModel
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly ILogger<LoginModel> _logger;
private readonly IBasketService _basketService;
public LoginModel(SignInManager<ApplicationUser> signInManager, ILogger<LoginModel> logger, IBasketService basketService)
{
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly ILogger<LoginModel> _logger;
private readonly IBasketService _basketService;
_signInManager = signInManager;
_logger = logger;
_basketService = basketService;
}
public LoginModel(SignInManager<ApplicationUser> signInManager, ILogger<LoginModel> logger, IBasketService basketService)
[BindProperty]
public InputModel Input { get; set; }
public IList<AuthenticationScheme> ExternalLogins { get; set; }
public string ReturnUrl { get; set; }
[TempData]
public string ErrorMessage { get; set; }
public class InputModel
{
[Required]
[EmailAddress]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
[Display(Name = "Remember me?")]
public bool RememberMe { get; set; }
}
public async Task OnGetAsync(string returnUrl = null)
{
if (!string.IsNullOrEmpty(ErrorMessage))
{
_signInManager = signInManager;
_logger = logger;
_basketService = basketService;
ModelState.AddModelError(string.Empty, ErrorMessage);
}
[BindProperty]
public InputModel Input { get; set; }
returnUrl = returnUrl ?? Url.Content("~/");
public IList<AuthenticationScheme> ExternalLogins { get; set; }
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
public string ReturnUrl { get; set; }
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
[TempData]
public string ErrorMessage { get; set; }
ReturnUrl = returnUrl;
}
public class InputModel
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl = returnUrl ?? Url.Content("~/");
if (ModelState.IsValid)
{
[Required]
[EmailAddress]
public string Email { get; set; }
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
//var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true);
var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, false, true);
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
[Display(Name = "Remember me?")]
public bool RememberMe { get; set; }
}
public async Task OnGetAsync(string returnUrl = null)
{
if (!string.IsNullOrEmpty(ErrorMessage))
if (result.Succeeded)
{
ModelState.AddModelError(string.Empty, ErrorMessage);
_logger.LogInformation("User logged in.");
await TransferAnonymousBasketToUserAsync(Input.Email);
return LocalRedirect(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
}
if (result.IsLockedOut)
{
_logger.LogWarning("User account locked out.");
return RedirectToPage("./Lockout");
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return Page();
}
returnUrl = returnUrl ?? Url.Content("~/");
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
ReturnUrl = returnUrl;
}
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
// If we got this far, something failed, redisplay form
return Page();
}
private async Task TransferAnonymousBasketToUserAsync(string userName)
{
if (Request.Cookies.ContainsKey(Constants.BASKET_COOKIENAME))
{
returnUrl = returnUrl ?? Url.Content("~/");
if (ModelState.IsValid)
var anonymousId = Request.Cookies[Constants.BASKET_COOKIENAME];
if (Guid.TryParse(anonymousId, out var _))
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
//var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true);
var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, false, true);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
await TransferAnonymousBasketToUserAsync(Input.Email);
return LocalRedirect(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
}
if (result.IsLockedOut)
{
_logger.LogWarning("User account locked out.");
return RedirectToPage("./Lockout");
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return Page();
}
}
// If we got this far, something failed, redisplay form
return Page();
}
private async Task TransferAnonymousBasketToUserAsync(string userName)
{
if (Request.Cookies.ContainsKey(Constants.BASKET_COOKIENAME))
{
var anonymousId = Request.Cookies[Constants.BASKET_COOKIENAME];
if (Guid.TryParse(anonymousId, out var _))
{
await _basketService.TransferBasketAsync(anonymousId, userName);
}
Response.Cookies.Delete(Constants.BASKET_COOKIENAME);
await _basketService.TransferBasketAsync(anonymousId, userName);
}
Response.Cookies.Delete(Constants.BASKET_COOKIENAME);
}
}
}

View File

@@ -1,4 +1,8 @@
using Microsoft.AspNetCore.Authentication;
using System;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
@@ -7,51 +11,46 @@ using Microsoft.eShopWeb.Infrastructure.Identity;
using Microsoft.eShopWeb.Web.Configuration;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account
namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account;
//TODO : replace IMemoryCache by distributed cache if you are in multi-host scenario
public class LogoutModel : PageModel
{
//TODO : replace IMemoryCache by distributed cache if you are in multi-host scenario
public class LogoutModel : PageModel
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly ILogger<LogoutModel> _logger;
private readonly IMemoryCache _cache;
public LogoutModel(SignInManager<ApplicationUser> signInManager, ILogger<LogoutModel> logger, IMemoryCache cache)
{
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly ILogger<LogoutModel> _logger;
private readonly IMemoryCache _cache;
_signInManager = signInManager;
_logger = logger;
_cache = cache;
}
public LogoutModel(SignInManager<ApplicationUser> signInManager, ILogger<LogoutModel> logger, IMemoryCache cache)
public void OnGet()
{
}
public async Task<IActionResult> OnPost(string returnUrl = null)
{
await _signInManager.SignOutAsync();
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
var userId = _signInManager.Context.User.Claims.First(c => c.Type == ClaimTypes.Name);
var identityKey = _signInManager.Context.Request.Cookies[ConfigureCookieSettings.IdentifierCookieName];
_cache.Set($"{userId.Value}:{identityKey}", identityKey, new MemoryCacheEntryOptions
{
_signInManager = signInManager;
_logger = logger;
_cache = cache;
AbsoluteExpiration = DateTime.Now.AddMinutes(ConfigureCookieSettings.ValidityMinutesPeriod)
});
_logger.LogInformation("User logged out.");
if (returnUrl != null)
{
return LocalRedirect(returnUrl);
}
public void OnGet()
else
{
}
public async Task<IActionResult> OnPost(string returnUrl = null)
{
await _signInManager.SignOutAsync();
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
var userId = _signInManager.Context.User.Claims.First(c => c.Type == ClaimTypes.Name);
var identityKey = _signInManager.Context.Request.Cookies[ConfigureCookieSettings.IdentifierCookieName];
_cache.Set($"{userId.Value}:{identityKey}", identityKey, new MemoryCacheEntryOptions
{
AbsoluteExpiration = DateTime.Now.AddMinutes(ConfigureCookieSettings.ValidityMinutesPeriod)
});
_logger.LogInformation("User logged out.");
if (returnUrl != null)
{
return LocalRedirect(returnUrl);
}
else
{
return RedirectToPage("/Index");
}
return RedirectToPage("/Index");
}
}
}
}

View File

@@ -11,89 +11,88 @@ using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.eShopWeb.Infrastructure.Identity;
using Microsoft.Extensions.Logging;
namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account
namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account;
[AllowAnonymous]
public class RegisterModel : PageModel
{
[AllowAnonymous]
public class RegisterModel : PageModel
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;
private readonly ILogger<RegisterModel> _logger;
private readonly IEmailSender _emailSender;
public RegisterModel(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
ILogger<RegisterModel> logger,
IEmailSender emailSender)
{
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;
private readonly ILogger<RegisterModel> _logger;
private readonly IEmailSender _emailSender;
_userManager = userManager;
_signInManager = signInManager;
_logger = logger;
_emailSender = emailSender;
}
public RegisterModel(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
ILogger<RegisterModel> logger,
IEmailSender emailSender)
[BindProperty]
public InputModel Input { get; set; }
public string ReturnUrl { get; set; }
public class InputModel
{
[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; }
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
}
public void OnGet(string returnUrl = null)
{
ReturnUrl = returnUrl;
}
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl = returnUrl ?? Url.Content("~/");
if (ModelState.IsValid)
{
_userManager = userManager;
_signInManager = signInManager;
_logger = logger;
_emailSender = emailSender;
}
[BindProperty]
public InputModel Input { get; set; }
public string ReturnUrl { get; set; }
public class InputModel
{
[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; }
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
}
public void OnGet(string returnUrl = null)
{
ReturnUrl = returnUrl;
}
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl = returnUrl ?? Url.Content("~/");
if (ModelState.IsValid)
var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email };
var result = await _userManager.CreateAsync(user, Input.Password);
if (result.Succeeded)
{
var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email };
var result = await _userManager.CreateAsync(user, Input.Password);
if (result.Succeeded)
{
_logger.LogInformation("User created a new account with password.");
_logger.LogInformation("User created a new account with password.");
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.Page(
"/Account/ConfirmEmail",
pageHandler: null,
values: new { userId = user.Id, code = code },
protocol: Request.Scheme);
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.Page(
"/Account/ConfirmEmail",
pageHandler: null,
values: new { userId = user.Id, code = code },
protocol: Request.Scheme);
await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
$"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
await _signInManager.SignInAsync(user, isPersistent: false);
return LocalRedirect(returnUrl);
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
await _signInManager.SignInAsync(user, isPersistent: false);
return LocalRedirect(returnUrl);
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
// If we got this far, something failed, redisplay form
return Page();
}
// If we got this far, something failed, redisplay form
return Page();
}
}

View File

@@ -1,41 +1,40 @@
using Microsoft.AspNetCore.Builder;
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using System;
namespace Microsoft.eShopWeb.Web.Configuration
namespace Microsoft.eShopWeb.Web.Configuration;
public static class ConfigureCookieSettings
{
public static class ConfigureCookieSettings
{
public const int ValidityMinutesPeriod = 60;
public const string IdentifierCookieName = "EshopIdentifier";
public const int ValidityMinutesPeriod = 60;
public const string IdentifierCookieName = "EshopIdentifier";
public static IServiceCollection AddCookieSettings(this IServiceCollection services)
public static IServiceCollection AddCookieSettings(this IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
//TODO need to check that.
//options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.Strict;
});
services.ConfigureApplicationCookie(options =>
});
services.ConfigureApplicationCookie(options =>
{
options.EventsType = typeof(RevokeAuthenticationEvents);
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(ValidityMinutesPeriod);
options.LoginPath = "/Account/Login";
options.LogoutPath = "/Account/Logout";
options.Cookie = new CookieBuilder
{
options.EventsType = typeof(RevokeAuthenticationEvents);
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(ValidityMinutesPeriod);
options.LoginPath = "/Account/Login";
options.LogoutPath = "/Account/Logout";
options.Cookie = new CookieBuilder
{
Name = IdentifierCookieName,
IsEssential = true // required for auth to work without explicit user consent; adjust to suit your privacy policy
Name = IdentifierCookieName,
IsEssential = true // required for auth to work without explicit user consent; adjust to suit your privacy policy
};
});
});
services.AddScoped<RevokeAuthenticationEvents>();
services.AddScoped<RevokeAuthenticationEvents>();
return services;
}
return services;
}
}

View File

@@ -6,24 +6,23 @@ using Microsoft.eShopWeb.Infrastructure.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.eShopWeb.Web.Configuration
namespace Microsoft.eShopWeb.Web.Configuration;
public static class ConfigureCoreServices
{
public static class ConfigureCoreServices
public static IServiceCollection AddCoreServices(this IServiceCollection services,
IConfiguration configuration)
{
public static IServiceCollection AddCoreServices(this IServiceCollection services,
IConfiguration configuration)
{
services.AddScoped(typeof(IReadRepository<>), typeof(EfRepository<>));
services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>));
services.AddScoped(typeof(IReadRepository<>), typeof(EfRepository<>));
services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>));
services.AddScoped<IBasketService, BasketService>();
services.AddScoped<IOrderService, OrderService>();
services.AddScoped<IBasketQueryService, BasketQueryService>();
services.AddSingleton<IUriComposer>(new UriComposer(configuration.Get<CatalogSettings>()));
services.AddScoped(typeof(IAppLogger<>), typeof(LoggerAdapter<>));
services.AddTransient<IEmailSender, EmailSender>();
services.AddScoped<IBasketService, BasketService>();
services.AddScoped<IOrderService, OrderService>();
services.AddScoped<IBasketQueryService, BasketQueryService>();
services.AddSingleton<IUriComposer>(new UriComposer(configuration.Get<CatalogSettings>()));
services.AddScoped(typeof(IAppLogger<>), typeof(LoggerAdapter<>));
services.AddTransient<IEmailSender, EmailSender>();
return services;
}
return services;
}
}

View File

@@ -4,20 +4,19 @@ using Microsoft.eShopWeb.Web.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.eShopWeb.Web.Configuration
{
public static class ConfigureWebServices
{
public static IServiceCollection AddWebServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddMediatR(typeof(BasketViewModelService).Assembly);
services.AddScoped<IBasketViewModelService, BasketViewModelService>();
services.AddScoped<CatalogViewModelService>();
services.AddScoped<ICatalogItemViewModelService, CatalogItemViewModelService>();
services.Configure<CatalogSettings>(configuration);
services.AddScoped<ICatalogViewModelService, CachedCatalogViewModelService>();
namespace Microsoft.eShopWeb.Web.Configuration;
return services;
}
public static class ConfigureWebServices
{
public static IServiceCollection AddWebServices(this IServiceCollection services, IConfiguration configuration)
{
services.AddMediatR(typeof(BasketViewModelService).Assembly);
services.AddScoped<IBasketViewModelService, BasketViewModelService>();
services.AddScoped<CatalogViewModelService>();
services.AddScoped<ICatalogItemViewModelService, CatalogItemViewModelService>();
services.Configure<CatalogSettings>(configuration);
services.AddScoped<ICatalogViewModelService, CachedCatalogViewModelService>();
return services;
}
}

View File

@@ -1,36 +1,35 @@
using Microsoft.AspNetCore.Authentication;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Web.Configuration
namespace Microsoft.eShopWeb.Web.Configuration;
//TODO : replace IMemoryCache with a distributed cache if you are in multi-host scenario
public class RevokeAuthenticationEvents : CookieAuthenticationEvents
{
//TODO : replace IMemoryCache with a distributed cache if you are in multi-host scenario
public class RevokeAuthenticationEvents : CookieAuthenticationEvents
private readonly IMemoryCache _cache;
private readonly ILogger _logger;
public RevokeAuthenticationEvents(IMemoryCache cache, ILogger<RevokeAuthenticationEvents> logger)
{
private readonly IMemoryCache _cache;
private readonly ILogger _logger;
_cache = cache;
_logger = logger;
}
public RevokeAuthenticationEvents(IMemoryCache cache, ILogger<RevokeAuthenticationEvents> logger)
public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
{
var userId = context.Principal.Claims.First(c => c.Type == ClaimTypes.Name);
var identityKey = context.Request.Cookies[ConfigureCookieSettings.IdentifierCookieName];
if (_cache.TryGetValue($"{userId.Value}:{identityKey}", out var revokeKeys))
{
_cache = cache;
_logger = logger;
}
public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
{
var userId = context.Principal.Claims.First(c => c.Type == ClaimTypes.Name);
var identityKey = context.Request.Cookies[ConfigureCookieSettings.IdentifierCookieName];
if (_cache.TryGetValue($"{userId.Value}:{identityKey}", out var revokeKeys))
{
_logger.LogDebug($"Access has been revoked for: {userId.Value}.");
context.RejectPrincipal();
await context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
_logger.LogDebug($"Access has been revoked for: {userId.Value}.");
context.RejectPrincipal();
await context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}
}
}

View File

@@ -1,10 +1,9 @@
 namespace Microsoft.eShopWeb.Web
namespace Microsoft.eShopWeb.Web;
public static class Constants
{
public static class Constants
{
public const string BASKET_COOKIENAME = "eShop";
public const int ITEMS_PER_PAGE = 10;
public const string DEFAULT_USERNAME = "Guest";
public const string BASKET_ID = "BasketId";
}
public const string BASKET_COOKIENAME = "eShop";
public const int ITEMS_PER_PAGE = 10;
public const string DEFAULT_USERNAME = "Guest";
public const string BASKET_ID = "BasketId";
}

View File

@@ -1,10 +1,9 @@
using Microsoft.AspNetCore.Mvc;
namespace Microsoft.eShopWeb.Web.Controllers.Api
{
// No longer used - shown for reference only if using full controllers instead of Endpoints for APIs
[Route("api/[controller]/[action]")]
[ApiController]
public class BaseApiController : ControllerBase
{ }
}
namespace Microsoft.eShopWeb.Web.Controllers.Api;
// No longer used - shown for reference only if using full controllers instead of Endpoints for APIs
[Route("api/[controller]/[action]")]
[ApiController]
public class BaseApiController : ControllerBase
{ }

View File

@@ -1,4 +1,9 @@
using Microsoft.AspNetCore.Authentication;
using System;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
@@ -6,495 +11,489 @@ using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Microsoft.eShopWeb.Infrastructure.Identity;
using Microsoft.eShopWeb.Web.Services;
using Microsoft.eShopWeb.Web.ViewModels.Manage;
using System;
using System.Linq;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Web.Controllers
namespace Microsoft.eShopWeb.Web.Controllers;
[ApiExplorerSettings(IgnoreApi = true)]
[Authorize] // Controllers that mainly require Authorization still use Controller/View; other pages use Pages
[Route("[controller]/[action]")]
public class ManageController : Controller
{
[ApiExplorerSettings(IgnoreApi = true)]
[Authorize] // Controllers that mainly require Authorization still use Controller/View; other pages use Pages
[Route("[controller]/[action]")]
public class ManageController : Controller
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IEmailSender _emailSender;
private readonly IAppLogger<ManageController> _logger;
private readonly UrlEncoder _urlEncoder;
private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
public ManageController(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
IEmailSender emailSender,
IAppLogger<ManageController> logger,
UrlEncoder urlEncoder)
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IEmailSender _emailSender;
private readonly IAppLogger<ManageController> _logger;
private readonly UrlEncoder _urlEncoder;
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
_logger = logger;
_urlEncoder = urlEncoder;
}
private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
[TempData]
public string StatusMessage { get; set; }
public ManageController(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
IEmailSender emailSender,
IAppLogger<ManageController> logger,
UrlEncoder urlEncoder)
[HttpGet]
public async Task<IActionResult> MyAccount()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
_userManager = userManager;
_signInManager = signInManager;
_emailSender = emailSender;
_logger = logger;
_urlEncoder = urlEncoder;
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
[TempData]
public string StatusMessage { get; set; }
[HttpGet]
public async Task<IActionResult> MyAccount()
var model = new IndexViewModel
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
Username = user.UserName,
Email = user.Email,
PhoneNumber = user.PhoneNumber,
IsEmailConfirmed = user.EmailConfirmed,
StatusMessage = StatusMessage
};
var model = new IndexViewModel
{
Username = user.UserName,
Email = user.Email,
PhoneNumber = user.PhoneNumber,
IsEmailConfirmed = user.EmailConfirmed,
StatusMessage = StatusMessage
};
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> MyAccount(IndexViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> MyAccount(IndexViewModel model)
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var email = user.Email;
if (model.Email != email)
{
var setEmailResult = await _userManager.SetEmailAsync(user, model.Email);
if (!setEmailResult.Succeeded)
{
throw new ApplicationException($"Unexpected error occurred setting email for user with ID '{user.Id}'.");
}
}
var phoneNumber = user.PhoneNumber;
if (model.PhoneNumber != phoneNumber)
{
var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, model.PhoneNumber);
if (!setPhoneResult.Succeeded)
{
throw new ApplicationException($"Unexpected error occurred setting phone number for user with ID '{user.Id}'.");
}
}
StatusMessage = "Your profile has been updated";
return RedirectToAction(nameof(MyAccount));
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SendVerificationEmail(IndexViewModel model)
var email = user.Email;
if (model.Email != email)
{
if (!ModelState.IsValid)
var setEmailResult = await _userManager.SetEmailAsync(user, model.Email);
if (!setEmailResult.Succeeded)
{
return View(model);
throw new ApplicationException($"Unexpected error occurred setting email for user with ID '{user.Id}'.");
}
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
var email = user.Email;
await _emailSender.SendEmailConfirmationAsync(email, callbackUrl);
StatusMessage = "Verification email sent. Please check your email.";
return RedirectToAction(nameof(MyAccount));
}
[HttpGet]
public async Task<IActionResult> ChangePassword()
var phoneNumber = user.PhoneNumber;
if (model.PhoneNumber != phoneNumber)
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, model.PhoneNumber);
if (!setPhoneResult.Succeeded)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
throw new ApplicationException($"Unexpected error occurred setting phone number for user with ID '{user.Id}'.");
}
}
var hasPassword = await _userManager.HasPasswordAsync(user);
if (!hasPassword)
{
return RedirectToAction(nameof(SetPassword));
}
StatusMessage = "Your profile has been updated";
return RedirectToAction(nameof(MyAccount));
}
var model = new ChangePasswordViewModel { StatusMessage = StatusMessage };
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SendVerificationEmail(IndexViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ChangePassword(ChangePasswordViewModel model)
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var changePasswordResult = await _userManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword);
if (!changePasswordResult.Succeeded)
{
AddErrors(changePasswordResult);
return View(model);
}
await _signInManager.SignInAsync(user, isPersistent: false);
_logger.LogInformation("User changed their password successfully.");
StatusMessage = "Your password has been changed.";
return RedirectToAction(nameof(ChangePassword));
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
[HttpGet]
public async Task<IActionResult> SetPassword()
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme);
var email = user.Email;
await _emailSender.SendEmailConfirmationAsync(email, callbackUrl);
StatusMessage = "Verification email sent. Please check your email.";
return RedirectToAction(nameof(MyAccount));
}
[HttpGet]
public async Task<IActionResult> ChangePassword()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var hasPassword = await _userManager.HasPasswordAsync(user);
if (hasPassword)
{
return RedirectToAction(nameof(ChangePassword));
}
var model = new SetPasswordViewModel { StatusMessage = StatusMessage };
return View(model);
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SetPassword(SetPasswordViewModel model)
var hasPassword = await _userManager.HasPasswordAsync(user);
if (!hasPassword)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var addPasswordResult = await _userManager.AddPasswordAsync(user, model.NewPassword);
if (!addPasswordResult.Succeeded)
{
AddErrors(addPasswordResult);
return View(model);
}
await _signInManager.SignInAsync(user, isPersistent: false);
StatusMessage = "Your password has been set.";
return RedirectToAction(nameof(SetPassword));
}
[HttpGet]
public async Task<IActionResult> ExternalLogins()
var model = new ChangePasswordViewModel { StatusMessage = StatusMessage };
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ChangePassword(ChangePasswordViewModel model)
{
if (!ModelState.IsValid)
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var model = new ExternalLoginsViewModel { CurrentLogins = await _userManager.GetLoginsAsync(user) };
model.OtherLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync())
.Where(auth => model.CurrentLogins.All(ul => auth.Name != ul.LoginProvider))
.ToList();
model.ShowRemoveButton = await _userManager.HasPasswordAsync(user) || model.CurrentLogins.Count > 1;
model.StatusMessage = StatusMessage;
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LinkLogin(string provider)
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
// Request a redirect to the external login provider to link a login for the current user
var redirectUrl = Url.Action(nameof(LinkLoginCallback));
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, _userManager.GetUserId(User));
return new ChallengeResult(provider, properties);
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
[HttpGet]
public async Task<IActionResult> LinkLoginCallback()
var changePasswordResult = await _userManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword);
if (!changePasswordResult.Succeeded)
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var info = await _signInManager.GetExternalLoginInfoAsync(user.Id);
if (info == null)
{
throw new ApplicationException($"Unexpected error occurred loading external login info for user with ID '{user.Id}'.");
}
var result = await _userManager.AddLoginAsync(user, info);
if (!result.Succeeded)
{
throw new ApplicationException($"Unexpected error occurred adding external login for user with ID '{user.Id}'.");
}
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
StatusMessage = "The external login was added.";
return RedirectToAction(nameof(ExternalLogins));
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> RemoveLogin(RemoveLoginViewModel model)
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var result = await _userManager.RemoveLoginAsync(user, model.LoginProvider, model.ProviderKey);
if (!result.Succeeded)
{
throw new ApplicationException($"Unexpected error occurred removing external login for user with ID '{user.Id}'.");
}
await _signInManager.SignInAsync(user, isPersistent: false);
StatusMessage = "The external login was removed.";
return RedirectToAction(nameof(ExternalLogins));
}
[HttpGet]
public async Task<IActionResult> TwoFactorAuthentication()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var model = new TwoFactorAuthenticationViewModel
{
HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null,
Is2faEnabled = user.TwoFactorEnabled,
RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user),
};
AddErrors(changePasswordResult);
return View(model);
}
[HttpGet]
public async Task<IActionResult> Disable2faWarning()
await _signInManager.SignInAsync(user, isPersistent: false);
_logger.LogInformation("User changed their password successfully.");
StatusMessage = "Your password has been changed.";
return RedirectToAction(nameof(ChangePassword));
}
[HttpGet]
public async Task<IActionResult> SetPassword()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
if (!user.TwoFactorEnabled)
{
throw new ApplicationException($"Unexpected error occured disabling 2FA for user with ID '{user.Id}'.");
}
return View(nameof(Disable2fa));
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Disable2fa()
var hasPassword = await _userManager.HasPasswordAsync(user);
if (hasPassword)
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false);
if (!disable2faResult.Succeeded)
{
throw new ApplicationException($"Unexpected error occured disabling 2FA for user with ID '{user.Id}'.");
}
_logger.LogInformation("User with ID {UserId} has disabled 2fa.", user.Id);
return RedirectToAction(nameof(TwoFactorAuthentication));
return RedirectToAction(nameof(ChangePassword));
}
[HttpGet]
public async Task<IActionResult> EnableAuthenticator()
var model = new SetPasswordViewModel { StatusMessage = StatusMessage };
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SetPassword(SetPasswordViewModel model)
{
if (!ModelState.IsValid)
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
if (string.IsNullOrEmpty(unformattedKey))
{
await _userManager.ResetAuthenticatorKeyAsync(user);
unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
}
var model = new EnableAuthenticatorViewModel
{
SharedKey = FormatKey(unformattedKey),
AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey)
};
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EnableAuthenticator(EnableAuthenticatorViewModel model)
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
// Strip spaces and hypens
var verificationCode = model.Code.Replace(" ", string.Empty).Replace("-", string.Empty);
var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
if (!is2faTokenValid)
{
ModelState.AddModelError("model.TwoFactorCode", "Verification code is invalid.");
return View(model);
}
await _userManager.SetTwoFactorEnabledAsync(user, true);
_logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id);
return RedirectToAction(nameof(GenerateRecoveryCodes));
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
[HttpGet]
public IActionResult ResetAuthenticatorWarning()
var addPasswordResult = await _userManager.AddPasswordAsync(user, model.NewPassword);
if (!addPasswordResult.Succeeded)
{
return View(nameof(ResetAuthenticator));
AddErrors(addPasswordResult);
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ResetAuthenticator()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
await _signInManager.SignInAsync(user, isPersistent: false);
StatusMessage = "Your password has been set.";
await _userManager.SetTwoFactorEnabledAsync(user, false);
return RedirectToAction(nameof(SetPassword));
}
[HttpGet]
public async Task<IActionResult> ExternalLogins()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var model = new ExternalLoginsViewModel { CurrentLogins = await _userManager.GetLoginsAsync(user) };
model.OtherLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync())
.Where(auth => model.CurrentLogins.All(ul => auth.Name != ul.LoginProvider))
.ToList();
model.ShowRemoveButton = await _userManager.HasPasswordAsync(user) || model.CurrentLogins.Count > 1;
model.StatusMessage = StatusMessage;
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LinkLogin(string provider)
{
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
// Request a redirect to the external login provider to link a login for the current user
var redirectUrl = Url.Action(nameof(LinkLoginCallback));
var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, _userManager.GetUserId(User));
return new ChallengeResult(provider, properties);
}
[HttpGet]
public async Task<IActionResult> LinkLoginCallback()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var info = await _signInManager.GetExternalLoginInfoAsync(user.Id);
if (info == null)
{
throw new ApplicationException($"Unexpected error occurred loading external login info for user with ID '{user.Id}'.");
}
var result = await _userManager.AddLoginAsync(user, info);
if (!result.Succeeded)
{
throw new ApplicationException($"Unexpected error occurred adding external login for user with ID '{user.Id}'.");
}
// Clear the existing external cookie to ensure a clean login process
await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);
StatusMessage = "The external login was added.";
return RedirectToAction(nameof(ExternalLogins));
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> RemoveLogin(RemoveLoginViewModel model)
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var result = await _userManager.RemoveLoginAsync(user, model.LoginProvider, model.ProviderKey);
if (!result.Succeeded)
{
throw new ApplicationException($"Unexpected error occurred removing external login for user with ID '{user.Id}'.");
}
await _signInManager.SignInAsync(user, isPersistent: false);
StatusMessage = "The external login was removed.";
return RedirectToAction(nameof(ExternalLogins));
}
[HttpGet]
public async Task<IActionResult> TwoFactorAuthentication()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var model = new TwoFactorAuthenticationViewModel
{
HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null,
Is2faEnabled = user.TwoFactorEnabled,
RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user),
};
return View(model);
}
[HttpGet]
public async Task<IActionResult> Disable2faWarning()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
if (!user.TwoFactorEnabled)
{
throw new ApplicationException($"Unexpected error occured disabling 2FA for user with ID '{user.Id}'.");
}
return View(nameof(Disable2fa));
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Disable2fa()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false);
if (!disable2faResult.Succeeded)
{
throw new ApplicationException($"Unexpected error occured disabling 2FA for user with ID '{user.Id}'.");
}
_logger.LogInformation("User with ID {UserId} has disabled 2fa.", user.Id);
return RedirectToAction(nameof(TwoFactorAuthentication));
}
[HttpGet]
public async Task<IActionResult> EnableAuthenticator()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
if (string.IsNullOrEmpty(unformattedKey))
{
await _userManager.ResetAuthenticatorKeyAsync(user);
_logger.LogInformation("User with id '{UserId}' has reset their authentication app key.", user.Id);
return RedirectToAction(nameof(EnableAuthenticator));
unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user);
}
[HttpGet]
public async Task<IActionResult> GenerateRecoveryCodes()
var model = new EnableAuthenticatorViewModel
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
SharedKey = FormatKey(unformattedKey),
AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey)
};
if (!user.TwoFactorEnabled)
{
throw new ApplicationException($"Cannot generate recovery codes for user with ID '{user.Id}' as they do not have 2FA enabled.");
}
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
var model = new GenerateRecoveryCodesViewModel { RecoveryCodes = recoveryCodes.ToArray() };
_logger.LogInformation("User with ID {UserId} has generated new 2FA recovery codes.", user.Id);
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EnableAuthenticator(EnableAuthenticatorViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
private void AddErrors(IdentityResult result)
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
private string FormatKey(string unformattedKey)
{
var result = new StringBuilder();
int currentPosition = 0;
while (currentPosition + 4 < unformattedKey.Length)
{
result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" ");
currentPosition += 4;
}
if (currentPosition < unformattedKey.Length)
{
result.Append(unformattedKey.Substring(currentPosition));
}
// Strip spaces and hypens
var verificationCode = model.Code.Replace(" ", string.Empty).Replace("-", string.Empty);
return result.ToString().ToLowerInvariant();
var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync(
user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
if (!is2faTokenValid)
{
ModelState.AddModelError("model.TwoFactorCode", "Verification code is invalid.");
return View(model);
}
private string GenerateQrCodeUri(string email, string unformattedKey)
await _userManager.SetTwoFactorEnabledAsync(user, true);
_logger.LogInformation("User with ID {UserId} has enabled 2FA with an authenticator app.", user.Id);
return RedirectToAction(nameof(GenerateRecoveryCodes));
}
[HttpGet]
public IActionResult ResetAuthenticatorWarning()
{
return View(nameof(ResetAuthenticator));
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ResetAuthenticator()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return string.Format(
AuthenticatorUriFormat,
_urlEncoder.Encode("eShopOnWeb"),
_urlEncoder.Encode(email),
unformattedKey);
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
await _userManager.SetTwoFactorEnabledAsync(user, false);
await _userManager.ResetAuthenticatorKeyAsync(user);
_logger.LogInformation("User with id '{UserId}' has reset their authentication app key.", user.Id);
return RedirectToAction(nameof(EnableAuthenticator));
}
[HttpGet]
public async Task<IActionResult> GenerateRecoveryCodes()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
if (!user.TwoFactorEnabled)
{
throw new ApplicationException($"Cannot generate recovery codes for user with ID '{user.Id}' as they do not have 2FA enabled.");
}
var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
var model = new GenerateRecoveryCodesViewModel { RecoveryCodes = recoveryCodes.ToArray() };
_logger.LogInformation("User with ID {UserId} has generated new 2FA recovery codes.", user.Id);
return View(model);
}
private void AddErrors(IdentityResult result)
{
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
private string FormatKey(string unformattedKey)
{
var result = new StringBuilder();
int currentPosition = 0;
while (currentPosition + 4 < unformattedKey.Length)
{
result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" ");
currentPosition += 4;
}
if (currentPosition < unformattedKey.Length)
{
result.Append(unformattedKey.Substring(currentPosition));
}
return result.ToString().ToLowerInvariant();
}
private string GenerateQrCodeUri(string email, string unformattedKey)
{
return string.Format(
AuthenticatorUriFormat,
_urlEncoder.Encode("eShopOnWeb"),
_urlEncoder.Encode(email),
unformattedKey);
}
}

View File

@@ -1,43 +1,42 @@
using MediatR;
using System.Threading.Tasks;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopWeb.Web.Features.MyOrders;
using Microsoft.eShopWeb.Web.Features.OrderDetails;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Web.Controllers
namespace Microsoft.eShopWeb.Web.Controllers;
[ApiExplorerSettings(IgnoreApi = true)]
[Authorize] // Controllers that mainly require Authorization still use Controller/View; other pages use Pages
[Route("[controller]/[action]")]
public class OrderController : Controller
{
[ApiExplorerSettings(IgnoreApi = true)]
[Authorize] // Controllers that mainly require Authorization still use Controller/View; other pages use Pages
[Route("[controller]/[action]")]
public class OrderController : Controller
private readonly IMediator _mediator;
public OrderController(IMediator mediator)
{
private readonly IMediator _mediator;
_mediator = mediator;
}
public OrderController(IMediator mediator)
[HttpGet]
public async Task<IActionResult> MyOrders()
{
var viewModel = await _mediator.Send(new GetMyOrders(User.Identity.Name));
return View(viewModel);
}
[HttpGet("{orderId}")]
public async Task<IActionResult> Detail(int orderId)
{
var viewModel = await _mediator.Send(new GetOrderDetails(User.Identity.Name, orderId));
if (viewModel == null)
{
_mediator = mediator;
return BadRequest("No such order found for this user.");
}
[HttpGet]
public async Task<IActionResult> MyOrders()
{
var viewModel = await _mediator.Send(new GetMyOrders(User.Identity.Name));
return View(viewModel);
}
[HttpGet("{orderId}")]
public async Task<IActionResult> Detail(int orderId)
{
var viewModel = await _mediator.Send(new GetOrderDetails(User.Identity.Name, orderId));
if (viewModel == null)
{
return BadRequest("No such order found for this user.");
}
return View(viewModel);
}
return View(viewModel);
}
}

View File

@@ -1,75 +1,74 @@
using BlazorShared.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using BlazorShared.Authorization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
namespace Microsoft.eShopWeb.Web.Controllers
namespace Microsoft.eShopWeb.Web.Controllers;
[Route("[controller]")]
[ApiController]
public class UserController : ControllerBase
{
[Route("[controller]")]
[ApiController]
public class UserController : ControllerBase
private readonly ITokenClaimsService _tokenClaimsService;
public UserController(ITokenClaimsService tokenClaimsService)
{
private readonly ITokenClaimsService _tokenClaimsService;
public UserController(ITokenClaimsService tokenClaimsService)
{
_tokenClaimsService = tokenClaimsService;
}
[HttpGet]
[Authorize]
[AllowAnonymous]
public async Task<IActionResult> GetCurrentUser() =>
Ok(User.Identity.IsAuthenticated ? await CreateUserInfo(User) : UserInfo.Anonymous);
private async Task<UserInfo> CreateUserInfo(ClaimsPrincipal claimsPrincipal)
{
if (!claimsPrincipal.Identity.IsAuthenticated)
{
return UserInfo.Anonymous;
}
var userInfo = new UserInfo
{
IsAuthenticated = true
};
if (claimsPrincipal.Identity is ClaimsIdentity claimsIdentity)
{
userInfo.NameClaimType = claimsIdentity.NameClaimType;
userInfo.RoleClaimType = claimsIdentity.RoleClaimType;
}
else
{
userInfo.NameClaimType = "name";
userInfo.RoleClaimType = "role";
}
if (claimsPrincipal.Claims.Any())
{
var claims = new List<ClaimValue>();
var nameClaims = claimsPrincipal.FindAll(userInfo.NameClaimType);
foreach (var claim in nameClaims)
{
claims.Add(new ClaimValue(userInfo.NameClaimType, claim.Value));
}
foreach (var claim in claimsPrincipal.Claims.Except(nameClaims))
{
claims.Add(new ClaimValue(claim.Type, claim.Value));
}
userInfo.Claims = claims;
}
var token = await _tokenClaimsService.GetTokenAsync(claimsPrincipal.Identity.Name);
userInfo.Token = token;
return userInfo;
}
_tokenClaimsService = tokenClaimsService;
}
}
[HttpGet]
[Authorize]
[AllowAnonymous]
public async Task<IActionResult> GetCurrentUser() =>
Ok(User.Identity.IsAuthenticated ? await CreateUserInfo(User) : UserInfo.Anonymous);
private async Task<UserInfo> CreateUserInfo(ClaimsPrincipal claimsPrincipal)
{
if (!claimsPrincipal.Identity.IsAuthenticated)
{
return UserInfo.Anonymous;
}
var userInfo = new UserInfo
{
IsAuthenticated = true
};
if (claimsPrincipal.Identity is ClaimsIdentity claimsIdentity)
{
userInfo.NameClaimType = claimsIdentity.NameClaimType;
userInfo.RoleClaimType = claimsIdentity.RoleClaimType;
}
else
{
userInfo.NameClaimType = "name";
userInfo.RoleClaimType = "role";
}
if (claimsPrincipal.Claims.Any())
{
var claims = new List<ClaimValue>();
var nameClaims = claimsPrincipal.FindAll(userInfo.NameClaimType);
foreach (var claim in nameClaims)
{
claims.Add(new ClaimValue(userInfo.NameClaimType, claim.Value));
}
foreach (var claim in claimsPrincipal.Claims.Except(nameClaims))
{
claims.Add(new ClaimValue(claim.Type, claim.Value));
}
userInfo.Claims = claims;
}
var token = await _tokenClaimsService.GetTokenAsync(claimsPrincipal.Identity.Name);
userInfo.Token = token;
return userInfo;
}
}

View File

@@ -1,25 +1,24 @@
using System;
namespace Microsoft.eShopWeb.Web.Extensions
namespace Microsoft.eShopWeb.Web.Extensions;
public static class CacheHelpers
{
public static class CacheHelpers
public static readonly TimeSpan DefaultCacheDuration = TimeSpan.FromSeconds(30);
private static readonly string _itemsKeyTemplate = "items-{0}-{1}-{2}-{3}";
public static string GenerateCatalogItemCacheKey(int pageIndex, int itemsPage, int? brandId, int? typeId)
{
public static readonly TimeSpan DefaultCacheDuration = TimeSpan.FromSeconds(30);
private static readonly string _itemsKeyTemplate = "items-{0}-{1}-{2}-{3}";
return string.Format(_itemsKeyTemplate, pageIndex, itemsPage, brandId, typeId);
}
public static string GenerateCatalogItemCacheKey(int pageIndex, int itemsPage, int? brandId, int? typeId)
{
return string.Format(_itemsKeyTemplate, pageIndex, itemsPage, brandId, typeId);
}
public static string GenerateBrandsCacheKey()
{
return "brands";
}
public static string GenerateBrandsCacheKey()
{
return "brands";
}
public static string GenerateTypesCacheKey()
{
return "types";
}
public static string GenerateTypesCacheKey()
{
return "types";
}
}

View File

@@ -1,15 +1,14 @@
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using System.Text.Encodings.Web;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
namespace Microsoft.eShopWeb.Web.Services
namespace Microsoft.eShopWeb.Web.Services;
public static class EmailSenderExtensions
{
public static class EmailSenderExtensions
public static Task SendEmailConfirmationAsync(this IEmailSender emailSender, string email, string link)
{
public static Task SendEmailConfirmationAsync(this IEmailSender emailSender, string email, string link)
{
return emailSender.SendEmailAsync(email, "Confirm your email",
$"Please confirm your account by clicking this link: <a href='{HtmlEncoder.Default.Encode(link)}'>link</a>");
}
return emailSender.SendEmailAsync(email, "Confirm your email",
$"Please confirm your account by clicking this link: <a href='{HtmlEncoder.Default.Encode(link)}'>link</a>");
}
}

View File

@@ -1,14 +1,13 @@
namespace Microsoft.AspNetCore.Mvc
namespace Microsoft.AspNetCore.Mvc;
public static class UrlHelperExtensions
{
public static class UrlHelperExtensions
public static string EmailConfirmationLink(this IUrlHelper urlHelper, string userId, string code, string scheme)
{
public static string EmailConfirmationLink(this IUrlHelper urlHelper, string userId, string code, string scheme)
{
return urlHelper.Action(
action: "GET",
controller: "ConfirmEmail",
values: new { userId, code },
protocol: scheme);
}
return urlHelper.Action(
action: "GET",
controller: "ConfirmEmail",
values: new { userId, code },
protocol: scheme);
}
}

View File

@@ -1,16 +1,15 @@
using MediatR;
using System.Collections.Generic;
using MediatR;
using Microsoft.eShopWeb.Web.ViewModels;
using System.Collections.Generic;
namespace Microsoft.eShopWeb.Web.Features.MyOrders
namespace Microsoft.eShopWeb.Web.Features.MyOrders;
public class GetMyOrders : IRequest<IEnumerable<OrderViewModel>>
{
public class GetMyOrders : IRequest<IEnumerable<OrderViewModel>>
{
public string UserName { get; set; }
public string UserName { get; set; }
public GetMyOrders(string userName)
{
UserName = userName;
}
public GetMyOrders(string userName)
{
UserName = userName;
}
}

View File

@@ -1,45 +1,44 @@
using MediatR;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediatR;
using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Microsoft.eShopWeb.ApplicationCore.Specifications;
using Microsoft.eShopWeb.Web.ViewModels;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Web.Features.MyOrders
namespace Microsoft.eShopWeb.Web.Features.MyOrders;
public class GetMyOrdersHandler : IRequestHandler<GetMyOrders, IEnumerable<OrderViewModel>>
{
public class GetMyOrdersHandler : IRequestHandler<GetMyOrders, IEnumerable<OrderViewModel>>
private readonly IReadRepository<Order> _orderRepository;
public GetMyOrdersHandler(IReadRepository<Order> orderRepository)
{
private readonly IReadRepository<Order> _orderRepository;
_orderRepository = orderRepository;
}
public GetMyOrdersHandler(IReadRepository<Order> orderRepository)
public async Task<IEnumerable<OrderViewModel>> Handle(GetMyOrders request,
CancellationToken cancellationToken)
{
var specification = new CustomerOrdersWithItemsSpecification(request.UserName);
var orders = await _orderRepository.ListAsync(specification, cancellationToken);
return orders.Select(o => new OrderViewModel
{
_orderRepository = orderRepository;
}
public async Task<IEnumerable<OrderViewModel>> Handle(GetMyOrders request,
CancellationToken cancellationToken)
{
var specification = new CustomerOrdersWithItemsSpecification(request.UserName);
var orders = await _orderRepository.ListAsync(specification, cancellationToken);
return orders.Select(o => new OrderViewModel
OrderDate = o.OrderDate,
OrderItems = o.OrderItems?.Select(oi => new OrderItemViewModel()
{
OrderDate = o.OrderDate,
OrderItems = o.OrderItems?.Select(oi => new OrderItemViewModel()
{
PictureUrl = oi.ItemOrdered.PictureUri,
ProductId = oi.ItemOrdered.CatalogItemId,
ProductName = oi.ItemOrdered.ProductName,
UnitPrice = oi.UnitPrice,
Units = oi.Units
}).ToList(),
OrderNumber = o.Id,
ShippingAddress = o.ShipToAddress,
Total = o.Total()
});
}
PictureUrl = oi.ItemOrdered.PictureUri,
ProductId = oi.ItemOrdered.CatalogItemId,
ProductName = oi.ItemOrdered.ProductName,
UnitPrice = oi.UnitPrice,
Units = oi.Units
}).ToList(),
OrderNumber = o.Id,
ShippingAddress = o.ShipToAddress,
Total = o.Total()
});
}
}

View File

@@ -1,17 +1,16 @@
using MediatR;
using Microsoft.eShopWeb.Web.ViewModels;
namespace Microsoft.eShopWeb.Web.Features.OrderDetails
{
public class GetOrderDetails : IRequest<OrderViewModel>
{
public string UserName { get; set; }
public int OrderId { get; set; }
namespace Microsoft.eShopWeb.Web.Features.OrderDetails;
public GetOrderDetails(string userName, int orderId)
{
UserName = userName;
OrderId = orderId;
}
public class GetOrderDetails : IRequest<OrderViewModel>
{
public string UserName { get; set; }
public int OrderId { get; set; }
public GetOrderDetails(string userName, int orderId)
{
UserName = userName;
OrderId = orderId;
}
}

View File

@@ -1,49 +1,48 @@
using MediatR;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediatR;
using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Microsoft.eShopWeb.ApplicationCore.Specifications;
using Microsoft.eShopWeb.Web.ViewModels;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Web.Features.OrderDetails
namespace Microsoft.eShopWeb.Web.Features.OrderDetails;
public class GetOrderDetailsHandler : IRequestHandler<GetOrderDetails, OrderViewModel>
{
public class GetOrderDetailsHandler : IRequestHandler<GetOrderDetails, OrderViewModel>
private readonly IReadRepository<Order> _orderRepository;
public GetOrderDetailsHandler(IReadRepository<Order> orderRepository)
{
private readonly IReadRepository<Order> _orderRepository;
_orderRepository = orderRepository;
}
public GetOrderDetailsHandler(IReadRepository<Order> orderRepository)
public async Task<OrderViewModel> Handle(GetOrderDetails request,
CancellationToken cancellationToken)
{
var spec = new OrderWithItemsByIdSpec(request.OrderId);
var order = await _orderRepository.GetBySpecAsync(spec, cancellationToken);
if (order == null)
{
_orderRepository = orderRepository;
return null;
}
public async Task<OrderViewModel> Handle(GetOrderDetails request,
CancellationToken cancellationToken)
return new OrderViewModel
{
var spec = new OrderWithItemsByIdSpec(request.OrderId);
var order = await _orderRepository.GetBySpecAsync(spec, cancellationToken);
if (order == null)
OrderDate = order.OrderDate,
OrderItems = order.OrderItems.Select(oi => new OrderItemViewModel
{
return null;
}
return new OrderViewModel
{
OrderDate = order.OrderDate,
OrderItems = order.OrderItems.Select(oi => new OrderItemViewModel
{
PictureUrl = oi.ItemOrdered.PictureUri,
ProductId = oi.ItemOrdered.CatalogItemId,
ProductName = oi.ItemOrdered.ProductName,
UnitPrice = oi.UnitPrice,
Units = oi.Units
}).ToList(),
OrderNumber = order.Id,
ShippingAddress = order.ShipToAddress,
Total = order.Total()
};
}
PictureUrl = oi.ItemOrdered.PictureUri,
ProductId = oi.ItemOrdered.CatalogItemId,
ProductName = oi.ItemOrdered.ProductName,
UnitPrice = oi.UnitPrice,
Units = oi.Units
}).ToList(),
OrderNumber = order.Id,
ShippingAddress = order.ShipToAddress,
Total = order.Total()
};
}
}

View File

@@ -1,40 +1,39 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System.Net.Http;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace Microsoft.eShopWeb.Web.HealthChecks
namespace Microsoft.eShopWeb.Web.HealthChecks;
public class ApiHealthCheck : IHealthCheck
{
public class ApiHealthCheck : IHealthCheck
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly LinkGenerator _linkGenerator;
public ApiHealthCheck(IHttpContextAccessor httpContextAccessor, LinkGenerator linkGenerator)
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly LinkGenerator _linkGenerator;
_httpContextAccessor = httpContextAccessor;
_linkGenerator = linkGenerator;
}
public ApiHealthCheck(IHttpContextAccessor httpContextAccessor, LinkGenerator linkGenerator)
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default(CancellationToken))
{
var request = _httpContextAccessor.HttpContext.Request;
string apiLink = _linkGenerator.GetPathByAction("List", "Catalog");
string myUrl = request.Scheme + "://" + request.Host.ToString() + apiLink;
var client = new HttpClient();
var response = await client.GetAsync(myUrl);
var pageContents = await response.Content.ReadAsStringAsync();
if (pageContents.Contains(".NET Bot Black Sweatshirt"))
{
_httpContextAccessor = httpContextAccessor;
_linkGenerator = linkGenerator;
return HealthCheckResult.Healthy("The check indicates a healthy result.");
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default(CancellationToken))
{
var request = _httpContextAccessor.HttpContext.Request;
string apiLink = _linkGenerator.GetPathByAction("List", "Catalog");
string myUrl = request.Scheme + "://" + request.Host.ToString() + apiLink;
var client = new HttpClient();
var response = await client.GetAsync(myUrl);
var pageContents = await response.Content.ReadAsStringAsync();
if (pageContents.Contains(".NET Bot Black Sweatshirt"))
{
return HealthCheckResult.Healthy("The check indicates a healthy result.");
}
return HealthCheckResult.Unhealthy("The check indicates an unhealthy result.");
}
return HealthCheckResult.Unhealthy("The check indicates an unhealthy result.");
}
}

View File

@@ -1,36 +1,35 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System.Net.Http;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Diagnostics.HealthChecks;
namespace Microsoft.eShopWeb.Web.HealthChecks
namespace Microsoft.eShopWeb.Web.HealthChecks;
public class HomePageHealthCheck : IHealthCheck
{
public class HomePageHealthCheck : IHealthCheck
private readonly IHttpContextAccessor _httpContextAccessor;
public HomePageHealthCheck(IHttpContextAccessor httpContextAccessor)
{
private readonly IHttpContextAccessor _httpContextAccessor;
_httpContextAccessor = httpContextAccessor;
}
public HomePageHealthCheck(IHttpContextAccessor httpContextAccessor)
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default(CancellationToken))
{
var request = _httpContextAccessor.HttpContext.Request;
string myUrl = request.Scheme + "://" + request.Host.ToString();
var client = new HttpClient();
var response = await client.GetAsync(myUrl);
var pageContents = await response.Content.ReadAsStringAsync();
if (pageContents.Contains(".NET Bot Black Sweatshirt"))
{
_httpContextAccessor = httpContextAccessor;
return HealthCheckResult.Healthy("The check indicates a healthy result.");
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default(CancellationToken))
{
var request = _httpContextAccessor.HttpContext.Request;
string myUrl = request.Scheme + "://" + request.Host.ToString();
var client = new HttpClient();
var response = await client.GetAsync(myUrl);
var pageContents = await response.Content.ReadAsStringAsync();
if (pageContents.Contains(".NET Bot Black Sweatshirt"))
{
return HealthCheckResult.Healthy("The check indicates a healthy result.");
}
return HealthCheckResult.Unhealthy("The check indicates an unhealthy result.");
}
return HealthCheckResult.Unhealthy("The check indicates an unhealthy result.");
}
}

View File

@@ -1,15 +1,14 @@
using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate;
using System.Threading.Tasks;
using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate;
using Microsoft.eShopWeb.Web.Pages.Basket;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Web.Interfaces
namespace Microsoft.eShopWeb.Web.Interfaces;
public interface IBasketViewModelService
{
public interface IBasketViewModelService
{
Task<BasketViewModel> GetOrCreateBasketForUser(string userName);
Task<BasketViewModel> GetOrCreateBasketForUser(string userName);
Task<int> CountTotalBasketItems(string username);
Task<int> CountTotalBasketItems(string username);
Task<BasketViewModel> Map(Basket basket);
}
Task<BasketViewModel> Map(Basket basket);
}

View File

@@ -1,10 +1,9 @@
using Microsoft.eShopWeb.Web.ViewModels;
using System.Threading.Tasks;
using System.Threading.Tasks;
using Microsoft.eShopWeb.Web.ViewModels;
namespace Microsoft.eShopWeb.Web.Interfaces
namespace Microsoft.eShopWeb.Web.Interfaces;
public interface ICatalogItemViewModelService
{
public interface ICatalogItemViewModelService
{
Task UpdateCatalogItem(CatalogItemViewModel viewModel);
}
Task UpdateCatalogItem(CatalogItemViewModel viewModel);
}

View File

@@ -1,14 +1,13 @@
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.eShopWeb.Web.ViewModels;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.eShopWeb.Web.ViewModels;
namespace Microsoft.eShopWeb.Web.Services
namespace Microsoft.eShopWeb.Web.Services;
public interface ICatalogViewModelService
{
public interface ICatalogViewModelService
{
Task<CatalogIndexViewModel> GetCatalogItems(int pageIndex, int itemsPage, int? brandId, int? typeId);
Task<IEnumerable<SelectListItem>> GetBrands();
Task<IEnumerable<SelectListItem>> GetTypes();
}
Task<CatalogIndexViewModel> GetCatalogItems(int pageIndex, int itemsPage, int? brandId, int? typeId);
Task<IEnumerable<SelectListItem>> GetBrands();
Task<IEnumerable<SelectListItem>> GetTypes();
}

View File

@@ -1,39 +1,38 @@
using Microsoft.AspNetCore.Authorization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.eShopWeb.ApplicationCore.Constants;
using Microsoft.eShopWeb.Web.Interfaces;
using Microsoft.eShopWeb.Web.ViewModels;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Web.Pages.Admin
namespace Microsoft.eShopWeb.Web.Pages.Admin;
[Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS)]
public class EditCatalogItemModel : PageModel
{
[Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS)]
public class EditCatalogItemModel : PageModel
private readonly ICatalogItemViewModelService _catalogItemViewModelService;
public EditCatalogItemModel(ICatalogItemViewModelService catalogItemViewModelService)
{
private readonly ICatalogItemViewModelService _catalogItemViewModelService;
_catalogItemViewModelService = catalogItemViewModelService;
}
public EditCatalogItemModel(ICatalogItemViewModelService catalogItemViewModelService)
[BindProperty]
public CatalogItemViewModel CatalogModel { get; set; } = new CatalogItemViewModel();
public void OnGet(CatalogItemViewModel catalogModel)
{
CatalogModel = catalogModel;
}
public async Task<IActionResult> OnPostAsync()
{
if (ModelState.IsValid)
{
_catalogItemViewModelService = catalogItemViewModelService;
await _catalogItemViewModelService.UpdateCatalogItem(CatalogModel);
}
[BindProperty]
public CatalogItemViewModel CatalogModel { get; set; } = new CatalogItemViewModel();
public void OnGet(CatalogItemViewModel catalogModel)
{
CatalogModel = catalogModel;
}
public async Task<IActionResult> OnPostAsync()
{
if (ModelState.IsValid)
{
await _catalogItemViewModelService.UpdateCatalogItem(CatalogModel);
}
return RedirectToPage("/Admin/Index");
}
return RedirectToPage("/Admin/Index");
}
}

View File

@@ -1,20 +1,19 @@
using Microsoft.AspNetCore.Authorization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.eShopWeb.ApplicationCore.Constants;
using Microsoft.eShopWeb.Web.Extensions;
using Microsoft.eShopWeb.Web.Services;
using Microsoft.eShopWeb.Web.ViewModels;
using Microsoft.Extensions.Caching.Memory;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Web.Pages.Admin
namespace Microsoft.eShopWeb.Web.Pages.Admin;
[Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS)]
public class IndexModel : PageModel
{
[Authorize(Roles = BlazorShared.Authorization.Constants.Roles.ADMINISTRATORS)]
public class IndexModel : PageModel
public IndexModel()
{
public IndexModel()
{
}
}
}

View File

@@ -1,18 +1,17 @@
using System.ComponentModel.DataAnnotations;
namespace Microsoft.eShopWeb.Web.Pages.Basket
{
public class BasketItemViewModel
{
public int Id { get; set; }
public int CatalogItemId { get; set; }
public string ProductName { get; set; }
public decimal UnitPrice { get; set; }
public decimal OldUnitPrice { get; set; }
namespace Microsoft.eShopWeb.Web.Pages.Basket;
[Range(0, int.MaxValue, ErrorMessage = "Quantity must be bigger than 0")]
public int Quantity { get; set; }
public string PictureUrl { get; set; }
}
public class BasketItemViewModel
{
public int Id { get; set; }
public int CatalogItemId { get; set; }
public string ProductName { get; set; }
public decimal UnitPrice { get; set; }
public decimal OldUnitPrice { get; set; }
[Range(0, int.MaxValue, ErrorMessage = "Quantity must be bigger than 0")]
public int Quantity { get; set; }
public string PictureUrl { get; set; }
}

View File

@@ -2,17 +2,16 @@
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.eShopWeb.Web.Pages.Basket
{
public class BasketViewModel
{
public int Id { get; set; }
public List<BasketItemViewModel> Items { get; set; } = new List<BasketItemViewModel>();
public string BuyerId { get; set; }
namespace Microsoft.eShopWeb.Web.Pages.Basket;
public decimal Total()
{
return Math.Round(Items.Sum(x => x.UnitPrice * x.Quantity), 2);
}
public class BasketViewModel
{
public int Id { get; set; }
public List<BasketItemViewModel> Items { get; set; } = new List<BasketItemViewModel>();
public string BuyerId { get; set; }
public decimal Total()
{
return Math.Round(Items.Sum(x => x.UnitPrice * x.Quantity), 2);
}
}

View File

@@ -1,4 +1,8 @@
using Microsoft.AspNetCore.Authentication.Cookies;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
@@ -9,94 +13,89 @@ using Microsoft.eShopWeb.ApplicationCore.Exceptions;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Microsoft.eShopWeb.Infrastructure.Identity;
using Microsoft.eShopWeb.Web.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Web.Pages.Basket
namespace Microsoft.eShopWeb.Web.Pages.Basket;
[Authorize]
public class CheckoutModel : PageModel
{
[Authorize]
public class CheckoutModel : PageModel
private readonly IBasketService _basketService;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IOrderService _orderService;
private string _username = null;
private readonly IBasketViewModelService _basketViewModelService;
private readonly IAppLogger<CheckoutModel> _logger;
public CheckoutModel(IBasketService basketService,
IBasketViewModelService basketViewModelService,
SignInManager<ApplicationUser> signInManager,
IOrderService orderService,
IAppLogger<CheckoutModel> logger)
{
private readonly IBasketService _basketService;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly IOrderService _orderService;
private string _username = null;
private readonly IBasketViewModelService _basketViewModelService;
private readonly IAppLogger<CheckoutModel> _logger;
_basketService = basketService;
_signInManager = signInManager;
_orderService = orderService;
_basketViewModelService = basketViewModelService;
_logger = logger;
}
public CheckoutModel(IBasketService basketService,
IBasketViewModelService basketViewModelService,
SignInManager<ApplicationUser> signInManager,
IOrderService orderService,
IAppLogger<CheckoutModel> logger)
{
_basketService = basketService;
_signInManager = signInManager;
_orderService = orderService;
_basketViewModelService = basketViewModelService;
_logger = logger;
}
public BasketViewModel BasketModel { get; set; } = new BasketViewModel();
public BasketViewModel BasketModel { get; set; } = new BasketViewModel();
public async Task OnGet()
{
await SetBasketModelAsync();
}
public async Task OnGet()
public async Task<IActionResult> OnPost(IEnumerable<BasketItemViewModel> items)
{
try
{
await SetBasketModelAsync();
if (!ModelState.IsValid)
{
return BadRequest();
}
var updateModel = items.ToDictionary(b => b.Id.ToString(), b => b.Quantity);
await _basketService.SetQuantities(BasketModel.Id, updateModel);
await _orderService.CreateOrderAsync(BasketModel.Id, new Address("123 Main St.", "Kent", "OH", "United States", "44240"));
await _basketService.DeleteBasketAsync(BasketModel.Id);
}
catch (EmptyBasketOnCheckoutException emptyBasketOnCheckoutException)
{
//Redirect to Empty Basket page
_logger.LogWarning(emptyBasketOnCheckoutException.Message);
return RedirectToPage("/Basket/Index");
}
public async Task<IActionResult> OnPost(IEnumerable<BasketItemViewModel> items)
return RedirectToPage("Success");
}
private async Task SetBasketModelAsync()
{
if (_signInManager.IsSignedIn(HttpContext.User))
{
try
{
await SetBasketModelAsync();
if (!ModelState.IsValid)
{
return BadRequest();
}
var updateModel = items.ToDictionary(b => b.Id.ToString(), b => b.Quantity);
await _basketService.SetQuantities(BasketModel.Id, updateModel);
await _orderService.CreateOrderAsync(BasketModel.Id, new Address("123 Main St.", "Kent", "OH", "United States", "44240"));
await _basketService.DeleteBasketAsync(BasketModel.Id);
}
catch (EmptyBasketOnCheckoutException emptyBasketOnCheckoutException)
{
//Redirect to Empty Basket page
_logger.LogWarning(emptyBasketOnCheckoutException.Message);
return RedirectToPage("/Basket/Index");
}
return RedirectToPage("Success");
BasketModel = await _basketViewModelService.GetOrCreateBasketForUser(User.Identity.Name);
}
private async Task SetBasketModelAsync()
else
{
if (_signInManager.IsSignedIn(HttpContext.User))
{
BasketModel = await _basketViewModelService.GetOrCreateBasketForUser(User.Identity.Name);
}
else
{
GetOrSetBasketCookieAndUserName();
BasketModel = await _basketViewModelService.GetOrCreateBasketForUser(_username);
}
}
private void GetOrSetBasketCookieAndUserName()
{
if (Request.Cookies.ContainsKey(Constants.BASKET_COOKIENAME))
{
_username = Request.Cookies[Constants.BASKET_COOKIENAME];
}
if (_username != null) return;
_username = Guid.NewGuid().ToString();
var cookieOptions = new CookieOptions();
cookieOptions.Expires = DateTime.Today.AddYears(10);
Response.Cookies.Append(Constants.BASKET_COOKIENAME, _username, cookieOptions);
GetOrSetBasketCookieAndUserName();
BasketModel = await _basketViewModelService.GetOrCreateBasketForUser(_username);
}
}
private void GetOrSetBasketCookieAndUserName()
{
if (Request.Cookies.ContainsKey(Constants.BASKET_COOKIENAME))
{
_username = Request.Cookies[Constants.BASKET_COOKIENAME];
}
if (_username != null) return;
_username = Guid.NewGuid().ToString();
var cookieOptions = new CookieOptions();
cookieOptions.Expires = DateTime.Today.AddYears(10);
Response.Cookies.Append(Constants.BASKET_COOKIENAME, _username, cookieOptions);
}
}

View File

@@ -1,93 +1,92 @@
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Microsoft.eShopWeb.Web.Interfaces;
using Microsoft.eShopWeb.Web.ViewModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Web.Pages.Basket
namespace Microsoft.eShopWeb.Web.Pages.Basket;
public class IndexModel : PageModel
{
public class IndexModel : PageModel
private readonly IBasketService _basketService;
private readonly IBasketViewModelService _basketViewModelService;
public IndexModel(IBasketService basketService,
IBasketViewModelService basketViewModelService)
{
private readonly IBasketService _basketService;
private readonly IBasketViewModelService _basketViewModelService;
_basketService = basketService;
_basketViewModelService = basketViewModelService;
}
public IndexModel(IBasketService basketService,
IBasketViewModelService basketViewModelService)
public BasketViewModel BasketModel { get; set; } = new BasketViewModel();
public async Task OnGet()
{
BasketModel = await _basketViewModelService.GetOrCreateBasketForUser(GetOrSetBasketCookieAndUserName());
}
public async Task<IActionResult> OnPost(CatalogItemViewModel productDetails)
{
if (productDetails?.Id == null)
{
_basketService = basketService;
_basketViewModelService = basketViewModelService;
return RedirectToPage("/Index");
}
public BasketViewModel BasketModel { get; set; } = new BasketViewModel();
var username = GetOrSetBasketCookieAndUserName();
var basket = await _basketService.AddItemToBasket(username,
productDetails.Id, productDetails.Price);
public async Task OnGet()
BasketModel = await _basketViewModelService.Map(basket);
return RedirectToPage();
}
public async Task OnPostUpdate(IEnumerable<BasketItemViewModel> items)
{
if (!ModelState.IsValid)
{
BasketModel = await _basketViewModelService.GetOrCreateBasketForUser(GetOrSetBasketCookieAndUserName());
return;
}
public async Task<IActionResult> OnPost(CatalogItemViewModel productDetails)
var basketView = await _basketViewModelService.GetOrCreateBasketForUser(GetOrSetBasketCookieAndUserName());
var updateModel = items.ToDictionary(b => b.Id.ToString(), b => b.Quantity);
var basket = await _basketService.SetQuantities(basketView.Id, updateModel);
BasketModel = await _basketViewModelService.Map(basket);
}
private string GetOrSetBasketCookieAndUserName()
{
string userName = null;
if (Request.HttpContext.User.Identity.IsAuthenticated)
{
if (productDetails?.Id == null)
{
return RedirectToPage("/Index");
}
var username = GetOrSetBasketCookieAndUserName();
var basket = await _basketService.AddItemToBasket(username,
productDetails.Id, productDetails.Price);
BasketModel = await _basketViewModelService.Map(basket);
return RedirectToPage();
return Request.HttpContext.User.Identity.Name;
}
public async Task OnPostUpdate(IEnumerable<BasketItemViewModel> items)
if (Request.Cookies.ContainsKey(Constants.BASKET_COOKIENAME))
{
if (!ModelState.IsValid)
userName = Request.Cookies[Constants.BASKET_COOKIENAME];
if (!Request.HttpContext.User.Identity.IsAuthenticated)
{
return;
}
var basketView = await _basketViewModelService.GetOrCreateBasketForUser(GetOrSetBasketCookieAndUserName());
var updateModel = items.ToDictionary(b => b.Id.ToString(), b => b.Quantity);
var basket = await _basketService.SetQuantities(basketView.Id, updateModel);
BasketModel = await _basketViewModelService.Map(basket);
}
private string GetOrSetBasketCookieAndUserName()
{
string userName = null;
if (Request.HttpContext.User.Identity.IsAuthenticated)
{
return Request.HttpContext.User.Identity.Name;
}
if (Request.Cookies.ContainsKey(Constants.BASKET_COOKIENAME))
{
userName = Request.Cookies[Constants.BASKET_COOKIENAME];
if (!Request.HttpContext.User.Identity.IsAuthenticated)
if (!Guid.TryParse(userName, out var _))
{
if (!Guid.TryParse(userName, out var _))
{
userName = null;
}
userName = null;
}
}
if (userName != null) return userName;
userName = Guid.NewGuid().ToString();
var cookieOptions = new CookieOptions { IsEssential = true };
cookieOptions.Expires = DateTime.Today.AddYears(10);
Response.Cookies.Append(Constants.BASKET_COOKIENAME, userName, cookieOptions);
return userName;
}
if (userName != null) return userName;
userName = Guid.NewGuid().ToString();
var cookieOptions = new CookieOptions { IsEssential = true };
cookieOptions.Expires = DateTime.Today.AddYears(10);
Response.Cookies.Append(Constants.BASKET_COOKIENAME, userName, cookieOptions);
return userName;
}
}

View File

@@ -6,14 +6,13 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microsoft.eShopWeb.Web.Pages.Basket
{
[Authorize]
public class SuccessModel : PageModel
{
public void OnGet()
{
namespace Microsoft.eShopWeb.Web.Pages.Basket;
[Authorize]
public class SuccessModel : PageModel
{
public void OnGet()
{
}
}
}
}

View File

@@ -1,19 +1,18 @@
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Diagnostics;
namespace Microsoft.eShopWeb.Web.Pages
namespace Microsoft.eShopWeb.Web.Pages;
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class ErrorModel : PageModel
{
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public class ErrorModel : PageModel
public string RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
public void OnGet()
{
public string RequestId { get; set; }
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
public void OnGet()
{
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
}
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
}
}

View File

@@ -1,24 +1,23 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.eShopWeb.Web.Services;
using Microsoft.eShopWeb.Web.ViewModels;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Web.Pages
namespace Microsoft.eShopWeb.Web.Pages;
public class IndexModel : PageModel
{
public class IndexModel : PageModel
private readonly ICatalogViewModelService _catalogViewModelService;
public IndexModel(ICatalogViewModelService catalogViewModelService)
{
private readonly ICatalogViewModelService _catalogViewModelService;
_catalogViewModelService = catalogViewModelService;
}
public IndexModel(ICatalogViewModelService catalogViewModelService)
{
_catalogViewModelService = catalogViewModelService;
}
public CatalogIndexViewModel CatalogModel { get; set; } = new CatalogIndexViewModel();
public CatalogIndexViewModel CatalogModel { get; set; } = new CatalogIndexViewModel();
public async Task OnGet(CatalogIndexViewModel catalogModel, int? pageId)
{
CatalogModel = await _catalogViewModelService.GetCatalogItems(pageId ?? 0, Constants.ITEMS_PER_PAGE, catalogModel.BrandFilterApplied, catalogModel.TypesFilterApplied);
}
public async Task OnGet(CatalogIndexViewModel catalogModel, int? pageId)
{
CatalogModel = await _catalogViewModelService.GetCatalogItems(pageId ?? 0, Constants.ITEMS_PER_PAGE, catalogModel.BrandFilterApplied, catalogModel.TypesFilterApplied);
}
}

View File

@@ -1,11 +1,10 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Microsoft.eShopWeb.Web.Pages
namespace Microsoft.eShopWeb.Web.Pages;
public class PrivacyModel : PageModel
{
public class PrivacyModel : PageModel
public void OnGet()
{
public void OnGet()
{
}
}
}

View File

@@ -1,60 +1,59 @@
using Microsoft.AspNetCore.Identity;
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.eShopWeb.Infrastructure.Identity;
using Microsoft.eShopWeb.Web.Interfaces;
using Microsoft.eShopWeb.Web.ViewModels;
using System;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Web.Pages.Shared.Components.BasketComponent
namespace Microsoft.eShopWeb.Web.Pages.Shared.Components.BasketComponent;
public class Basket : ViewComponent
{
public class Basket : ViewComponent
private readonly IBasketViewModelService _basketService;
private readonly SignInManager<ApplicationUser> _signInManager;
public Basket(IBasketViewModelService basketService,
SignInManager<ApplicationUser> signInManager)
{
private readonly IBasketViewModelService _basketService;
private readonly SignInManager<ApplicationUser> _signInManager;
_basketService = basketService;
_signInManager = signInManager;
}
public Basket(IBasketViewModelService basketService,
SignInManager<ApplicationUser> signInManager)
public async Task<IViewComponentResult> InvokeAsync()
{
var vm = new BasketComponentViewModel
{
_basketService = basketService;
_signInManager = signInManager;
ItemsCount = await CountTotalBasketItems()
};
return View(vm);
}
private async Task<int> CountTotalBasketItems()
{
if (_signInManager.IsSignedIn(HttpContext.User))
{
return await _basketService.CountTotalBasketItems(User.Identity.Name);
}
public async Task<IViewComponentResult> InvokeAsync()
{
var vm = new BasketComponentViewModel
{
ItemsCount = await CountTotalBasketItems()
};
return View(vm);
}
string anonymousId = GetAnnonymousIdFromCookie();
if (anonymousId == null)
return 0;
private async Task<int> CountTotalBasketItems()
return await _basketService.CountTotalBasketItems(anonymousId);
}
private string GetAnnonymousIdFromCookie()
{
if (Request.Cookies.ContainsKey(Constants.BASKET_COOKIENAME))
{
if (_signInManager.IsSignedIn(HttpContext.User))
var id = Request.Cookies[Constants.BASKET_COOKIENAME];
if (Guid.TryParse(id, out var _))
{
return await _basketService.CountTotalBasketItems(User.Identity.Name);
return id;
}
string anonymousId = GetAnnonymousIdFromCookie();
if (anonymousId == null)
return 0;
return await _basketService.CountTotalBasketItems(anonymousId);
}
private string GetAnnonymousIdFromCookie()
{
if (Request.Cookies.ContainsKey(Constants.BASKET_COOKIENAME))
{
var id = Request.Cookies[Constants.BASKET_COOKIENAME];
if (Guid.TryParse(id, out var _))
{
return id;
}
}
return null;
}
return null;
}
}

View File

@@ -1,50 +1,49 @@
using Microsoft.AspNetCore.Hosting;
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.eShopWeb.Infrastructure.Data;
using Microsoft.eShopWeb.Infrastructure.Identity;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Web
namespace Microsoft.eShopWeb.Web;
public class Program
{
public class Program
public static async Task Main(string[] args)
{
public static async Task Main(string[] args)
var host = CreateHostBuilder(args)
.Build();
using (var scope = host.Services.CreateScope())
{
var host = CreateHostBuilder(args)
.Build();
using (var scope = host.Services.CreateScope())
var services = scope.ServiceProvider;
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
try
{
var services = scope.ServiceProvider;
var loggerFactory = services.GetRequiredService<ILoggerFactory>();
try
{
var catalogContext = services.GetRequiredService<CatalogContext>();
await CatalogContextSeed.SeedAsync(catalogContext, loggerFactory);
var catalogContext = services.GetRequiredService<CatalogContext>();
await CatalogContextSeed.SeedAsync(catalogContext, loggerFactory);
var userManager = services.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = services.GetRequiredService<RoleManager<IdentityRole>>();
await AppIdentityDbContextSeed.SeedAsync(userManager, roleManager);
}
catch (Exception ex)
{
var logger = loggerFactory.CreateLogger<Program>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
var userManager = services.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = services.GetRequiredService<RoleManager<IdentityRole>>();
await AppIdentityDbContextSeed.SeedAsync(userManager, roleManager);
}
catch (Exception ex)
{
var logger = loggerFactory.CreateLogger<Program>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
host.Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
host.Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}

View File

@@ -1,97 +1,96 @@
using Microsoft.eShopWeb.ApplicationCore.Entities;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.eShopWeb.ApplicationCore.Entities.BasketAggregate;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Microsoft.eShopWeb.ApplicationCore.Specifications;
using Microsoft.eShopWeb.Web.Interfaces;
using Microsoft.eShopWeb.Web.Pages.Basket;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Web.Services
namespace Microsoft.eShopWeb.Web.Services;
public class BasketViewModelService : IBasketViewModelService
{
public class BasketViewModelService : IBasketViewModelService
private readonly IRepository<Basket> _basketRepository;
private readonly IUriComposer _uriComposer;
private readonly IBasketQueryService _basketQueryService;
private readonly IRepository<CatalogItem> _itemRepository;
public BasketViewModelService(IRepository<Basket> basketRepository,
IRepository<CatalogItem> itemRepository,
IUriComposer uriComposer,
IBasketQueryService basketQueryService)
{
private readonly IRepository<Basket> _basketRepository;
private readonly IUriComposer _uriComposer;
private readonly IBasketQueryService _basketQueryService;
private readonly IRepository<CatalogItem> _itemRepository;
_basketRepository = basketRepository;
_uriComposer = uriComposer;
_basketQueryService = basketQueryService;
_itemRepository = itemRepository;
}
public BasketViewModelService(IRepository<Basket> basketRepository,
IRepository<CatalogItem> itemRepository,
IUriComposer uriComposer,
IBasketQueryService basketQueryService)
public async Task<BasketViewModel> GetOrCreateBasketForUser(string userName)
{
var basketSpec = new BasketWithItemsSpecification(userName);
var basket = (await _basketRepository.GetBySpecAsync(basketSpec));
if (basket == null)
{
_basketRepository = basketRepository;
_uriComposer = uriComposer;
_basketQueryService = basketQueryService;
_itemRepository = itemRepository;
return await CreateBasketForUser(userName);
}
var viewModel = await Map(basket);
return viewModel;
}
public async Task<BasketViewModel> GetOrCreateBasketForUser(string userName)
private async Task<BasketViewModel> CreateBasketForUser(string userId)
{
var basket = new Basket(userId);
await _basketRepository.AddAsync(basket);
return new BasketViewModel()
{
var basketSpec = new BasketWithItemsSpecification(userName);
var basket = (await _basketRepository.GetBySpecAsync(basketSpec));
BuyerId = basket.BuyerId,
Id = basket.Id,
};
}
if (basket == null)
{
return await CreateBasketForUser(userName);
}
var viewModel = await Map(basket);
return viewModel;
}
private async Task<List<BasketItemViewModel>> GetBasketItems(IReadOnlyCollection<BasketItem> basketItems)
{
var catalogItemsSpecification = new CatalogItemsSpecification(basketItems.Select(b => b.CatalogItemId).ToArray());
var catalogItems = await _itemRepository.ListAsync(catalogItemsSpecification);
private async Task<BasketViewModel> CreateBasketForUser(string userId)
var items = basketItems.Select(basketItem =>
{
var basket = new Basket(userId);
await _basketRepository.AddAsync(basket);
var catalogItem = catalogItems.First(c => c.Id == basketItem.CatalogItemId);
return new BasketViewModel()
var basketItemViewModel = new BasketItemViewModel
{
BuyerId = basket.BuyerId,
Id = basket.Id,
Id = basketItem.Id,
UnitPrice = basketItem.UnitPrice,
Quantity = basketItem.Quantity,
CatalogItemId = basketItem.CatalogItemId,
PictureUrl = _uriComposer.ComposePicUri(catalogItem.PictureUri),
ProductName = catalogItem.Name
};
}
return basketItemViewModel;
}).ToList();
private async Task<List<BasketItemViewModel>> GetBasketItems(IReadOnlyCollection<BasketItem> basketItems)
return items;
}
public async Task<BasketViewModel> Map(Basket basket)
{
return new BasketViewModel()
{
var catalogItemsSpecification = new CatalogItemsSpecification(basketItems.Select(b => b.CatalogItemId).ToArray());
var catalogItems = await _itemRepository.ListAsync(catalogItemsSpecification);
BuyerId = basket.BuyerId,
Id = basket.Id,
Items = await GetBasketItems(basket.Items)
};
}
var items = basketItems.Select(basketItem =>
{
var catalogItem = catalogItems.First(c => c.Id == basketItem.CatalogItemId);
public async Task<int> CountTotalBasketItems(string username)
{
var counter = await _basketQueryService.CountTotalBasketItems(username);
var basketItemViewModel = new BasketItemViewModel
{
Id = basketItem.Id,
UnitPrice = basketItem.UnitPrice,
Quantity = basketItem.Quantity,
CatalogItemId = basketItem.CatalogItemId,
PictureUrl = _uriComposer.ComposePicUri(catalogItem.PictureUri),
ProductName = catalogItem.Name
};
return basketItemViewModel;
}).ToList();
return items;
}
public async Task<BasketViewModel> Map(Basket basket)
{
return new BasketViewModel()
{
BuyerId = basket.BuyerId,
Id = basket.Id,
Items = await GetBasketItems(basket.Items)
};
}
public async Task<int> CountTotalBasketItems(string username)
{
var counter = await _basketQueryService.CountTotalBasketItems(username);
return counter;
}
return counter;
}
}

View File

@@ -1,51 +1,50 @@
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.eShopWeb.Web.Extensions;
using Microsoft.eShopWeb.Web.ViewModels;
using Microsoft.Extensions.Caching.Memory;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Web.Services
namespace Microsoft.eShopWeb.Web.Services;
public class CachedCatalogViewModelService : ICatalogViewModelService
{
public class CachedCatalogViewModelService : ICatalogViewModelService
private readonly IMemoryCache _cache;
private readonly CatalogViewModelService _catalogViewModelService;
public CachedCatalogViewModelService(IMemoryCache cache,
CatalogViewModelService catalogViewModelService)
{
private readonly IMemoryCache _cache;
private readonly CatalogViewModelService _catalogViewModelService;
_cache = cache;
_catalogViewModelService = catalogViewModelService;
}
public CachedCatalogViewModelService(IMemoryCache cache,
CatalogViewModelService catalogViewModelService)
public async Task<IEnumerable<SelectListItem>> GetBrands()
{
return await _cache.GetOrCreateAsync(CacheHelpers.GenerateBrandsCacheKey(), async entry =>
{
entry.SlidingExpiration = CacheHelpers.DefaultCacheDuration;
return await _catalogViewModelService.GetBrands();
});
}
public async Task<CatalogIndexViewModel> GetCatalogItems(int pageIndex, int itemsPage, int? brandId, int? typeId)
{
var cacheKey = CacheHelpers.GenerateCatalogItemCacheKey(pageIndex, Constants.ITEMS_PER_PAGE, brandId, typeId);
return await _cache.GetOrCreateAsync(cacheKey, async entry =>
{
_cache = cache;
_catalogViewModelService = catalogViewModelService;
}
entry.SlidingExpiration = CacheHelpers.DefaultCacheDuration;
return await _catalogViewModelService.GetCatalogItems(pageIndex, itemsPage, brandId, typeId);
});
}
public async Task<IEnumerable<SelectListItem>> GetBrands()
public async Task<IEnumerable<SelectListItem>> GetTypes()
{
return await _cache.GetOrCreateAsync(CacheHelpers.GenerateTypesCacheKey(), async entry =>
{
return await _cache.GetOrCreateAsync(CacheHelpers.GenerateBrandsCacheKey(), async entry =>
{
entry.SlidingExpiration = CacheHelpers.DefaultCacheDuration;
return await _catalogViewModelService.GetBrands();
});
}
public async Task<CatalogIndexViewModel> GetCatalogItems(int pageIndex, int itemsPage, int? brandId, int? typeId)
{
var cacheKey = CacheHelpers.GenerateCatalogItemCacheKey(pageIndex, Constants.ITEMS_PER_PAGE, brandId, typeId);
return await _cache.GetOrCreateAsync(cacheKey, async entry =>
{
entry.SlidingExpiration = CacheHelpers.DefaultCacheDuration;
return await _catalogViewModelService.GetCatalogItems(pageIndex, itemsPage, brandId, typeId);
});
}
public async Task<IEnumerable<SelectListItem>> GetTypes()
{
return await _cache.GetOrCreateAsync(CacheHelpers.GenerateTypesCacheKey(), async entry =>
{
entry.SlidingExpiration = CacheHelpers.DefaultCacheDuration;
return await _catalogViewModelService.GetTypes();
});
}
entry.SlidingExpiration = CacheHelpers.DefaultCacheDuration;
return await _catalogViewModelService.GetTypes();
});
}
}

View File

@@ -1,25 +1,24 @@
using Microsoft.eShopWeb.ApplicationCore.Entities;
using System.Threading.Tasks;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Microsoft.eShopWeb.Web.Interfaces;
using Microsoft.eShopWeb.Web.ViewModels;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Web.Services
namespace Microsoft.eShopWeb.Web.Services;
public class CatalogItemViewModelService : ICatalogItemViewModelService
{
public class CatalogItemViewModelService : ICatalogItemViewModelService
private readonly IRepository<CatalogItem> _catalogItemRepository;
public CatalogItemViewModelService(IRepository<CatalogItem> catalogItemRepository)
{
private readonly IRepository<CatalogItem> _catalogItemRepository;
_catalogItemRepository = catalogItemRepository;
}
public CatalogItemViewModelService(IRepository<CatalogItem> catalogItemRepository)
{
_catalogItemRepository = catalogItemRepository;
}
public async Task UpdateCatalogItem(CatalogItemViewModel viewModel)
{
var existingCatalogItem = await _catalogItemRepository.GetByIdAsync(viewModel.Id);
existingCatalogItem.UpdateDetails(viewModel.Name, existingCatalogItem.Description, viewModel.Price);
await _catalogItemRepository.UpdateAsync(existingCatalogItem);
}
public async Task UpdateCatalogItem(CatalogItemViewModel viewModel)
{
var existingCatalogItem = await _catalogItemRepository.GetByIdAsync(viewModel.Id);
existingCatalogItem.UpdateDetails(viewModel.Name, existingCatalogItem.Description, viewModel.Price);
await _catalogItemRepository.UpdateAsync(existingCatalogItem);
}
}

View File

@@ -1,112 +1,111 @@
using Microsoft.AspNetCore.Mvc.Rendering;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.eShopWeb.ApplicationCore.Entities;
using Microsoft.eShopWeb.ApplicationCore.Interfaces;
using Microsoft.eShopWeb.ApplicationCore.Specifications;
using Microsoft.eShopWeb.Web.ViewModels;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.eShopWeb.Web.Services
namespace Microsoft.eShopWeb.Web.Services;
/// <summary>
/// This is a UI-specific service so belongs in UI project. It does not contain any business logic and works
/// with UI-specific types (view models and SelectListItem types).
/// </summary>
public class CatalogViewModelService : ICatalogViewModelService
{
/// <summary>
/// This is a UI-specific service so belongs in UI project. It does not contain any business logic and works
/// with UI-specific types (view models and SelectListItem types).
/// </summary>
public class CatalogViewModelService : ICatalogViewModelService
private readonly ILogger<CatalogViewModelService> _logger;
private readonly IRepository<CatalogItem> _itemRepository;
private readonly IRepository<CatalogBrand> _brandRepository;
private readonly IRepository<CatalogType> _typeRepository;
private readonly IUriComposer _uriComposer;
public CatalogViewModelService(
ILoggerFactory loggerFactory,
IRepository<CatalogItem> itemRepository,
IRepository<CatalogBrand> brandRepository,
IRepository<CatalogType> typeRepository,
IUriComposer uriComposer)
{
private readonly ILogger<CatalogViewModelService> _logger;
private readonly IRepository<CatalogItem> _itemRepository;
private readonly IRepository<CatalogBrand> _brandRepository;
private readonly IRepository<CatalogType> _typeRepository;
private readonly IUriComposer _uriComposer;
_logger = loggerFactory.CreateLogger<CatalogViewModelService>();
_itemRepository = itemRepository;
_brandRepository = brandRepository;
_typeRepository = typeRepository;
_uriComposer = uriComposer;
}
public CatalogViewModelService(
ILoggerFactory loggerFactory,
IRepository<CatalogItem> itemRepository,
IRepository<CatalogBrand> brandRepository,
IRepository<CatalogType> typeRepository,
IUriComposer uriComposer)
public async Task<CatalogIndexViewModel> GetCatalogItems(int pageIndex, int itemsPage, int? brandId, int? typeId)
{
_logger.LogInformation("GetCatalogItems called.");
var filterSpecification = new CatalogFilterSpecification(brandId, typeId);
var filterPaginatedSpecification =
new CatalogFilterPaginatedSpecification(itemsPage * pageIndex, itemsPage, brandId, typeId);
// the implementation below using ForEach and Count. We need a List.
var itemsOnPage = await _itemRepository.ListAsync(filterPaginatedSpecification);
var totalItems = await _itemRepository.CountAsync(filterSpecification);
var vm = new CatalogIndexViewModel()
{
_logger = loggerFactory.CreateLogger<CatalogViewModelService>();
_itemRepository = itemRepository;
_brandRepository = brandRepository;
_typeRepository = typeRepository;
_uriComposer = uriComposer;
}
public async Task<CatalogIndexViewModel> GetCatalogItems(int pageIndex, int itemsPage, int? brandId, int? typeId)
{
_logger.LogInformation("GetCatalogItems called.");
var filterSpecification = new CatalogFilterSpecification(brandId, typeId);
var filterPaginatedSpecification =
new CatalogFilterPaginatedSpecification(itemsPage * pageIndex, itemsPage, brandId, typeId);
// the implementation below using ForEach and Count. We need a List.
var itemsOnPage = await _itemRepository.ListAsync(filterPaginatedSpecification);
var totalItems = await _itemRepository.CountAsync(filterSpecification);
var vm = new CatalogIndexViewModel()
CatalogItems = itemsOnPage.Select(i => new CatalogItemViewModel()
{
CatalogItems = itemsOnPage.Select(i => new CatalogItemViewModel()
{
Id = i.Id,
Name = i.Name,
PictureUri = _uriComposer.ComposePicUri(i.PictureUri),
Price = i.Price
}).ToList(),
Brands = (await GetBrands()).ToList(),
Types = (await GetTypes()).ToList(),
BrandFilterApplied = brandId ?? 0,
TypesFilterApplied = typeId ?? 0,
PaginationInfo = new PaginationInfoViewModel()
{
ActualPage = pageIndex,
ItemsPerPage = itemsOnPage.Count,
TotalItems = totalItems,
TotalPages = int.Parse(Math.Ceiling(((decimal)totalItems / itemsPage)).ToString())
}
};
Id = i.Id,
Name = i.Name,
PictureUri = _uriComposer.ComposePicUri(i.PictureUri),
Price = i.Price
}).ToList(),
Brands = (await GetBrands()).ToList(),
Types = (await GetTypes()).ToList(),
BrandFilterApplied = brandId ?? 0,
TypesFilterApplied = typeId ?? 0,
PaginationInfo = new PaginationInfoViewModel()
{
ActualPage = pageIndex,
ItemsPerPage = itemsOnPage.Count,
TotalItems = totalItems,
TotalPages = int.Parse(Math.Ceiling(((decimal)totalItems / itemsPage)).ToString())
}
};
vm.PaginationInfo.Next = (vm.PaginationInfo.ActualPage == vm.PaginationInfo.TotalPages - 1) ? "is-disabled" : "";
vm.PaginationInfo.Previous = (vm.PaginationInfo.ActualPage == 0) ? "is-disabled" : "";
vm.PaginationInfo.Next = (vm.PaginationInfo.ActualPage == vm.PaginationInfo.TotalPages - 1) ? "is-disabled" : "";
vm.PaginationInfo.Previous = (vm.PaginationInfo.ActualPage == 0) ? "is-disabled" : "";
return vm;
}
return vm;
}
public async Task<IEnumerable<SelectListItem>> GetBrands()
{
_logger.LogInformation("GetBrands called.");
var brands = await _brandRepository.ListAsync();
public async Task<IEnumerable<SelectListItem>> GetBrands()
{
_logger.LogInformation("GetBrands called.");
var brands = await _brandRepository.ListAsync();
var items = brands
.Select(brand => new SelectListItem() { Value = brand.Id.ToString(), Text = brand.Brand })
.OrderBy(b => b.Text)
.ToList();
var items = brands
.Select(brand => new SelectListItem() { Value = brand.Id.ToString(), Text = brand.Brand })
.OrderBy(b => b.Text)
.ToList();
var allItem = new SelectListItem() { Value = null, Text = "All", Selected = true };
items.Insert(0, allItem);
var allItem = new SelectListItem() { Value = null, Text = "All", Selected = true };
items.Insert(0, allItem);
return items;
}
return items;
}
public async Task<IEnumerable<SelectListItem>> GetTypes()
{
_logger.LogInformation("GetTypes called.");
var types = await _typeRepository.ListAsync();
public async Task<IEnumerable<SelectListItem>> GetTypes()
{
_logger.LogInformation("GetTypes called.");
var types = await _typeRepository.ListAsync();
var items = types
.Select(type => new SelectListItem() { Value = type.Id.ToString(), Text = type.Type })
.OrderBy(t => t.Text)
.ToList();
var items = types
.Select(type => new SelectListItem() { Value = type.Id.ToString(), Text = type.Type })
.OrderBy(t => t.Text)
.ToList();
var allItem = new SelectListItem() { Value = null, Text = "All", Selected = true };
items.Insert(0, allItem);
var allItem = new SelectListItem() { Value = null, Text = "All", Selected = true };
items.Insert(0, allItem);
return items;
}
return items;
}
}

View File

@@ -1,17 +1,15 @@
using Microsoft.AspNetCore.Routing;
using System.Text.RegularExpressions;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Routing;
namespace Microsoft.eShopWeb.Web
namespace Microsoft.eShopWeb.Web;
public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
public class SlugifyParameterTransformer : IOutboundParameterTransformer
public string TransformOutbound(object value)
{
public string TransformOutbound(object value)
{
if (value == null) { return null; }
if (value == null) { return null; }
// Slugify value
return Regex.Replace(value.ToString(), "([a-z])([A-Z])", "$1-$2").ToLower();
}
// Slugify value
return Regex.Replace(value.ToString(), "([a-z])([A-Z])", "$1-$2").ToLower();
}
}

View File

@@ -1,4 +1,10 @@
using Ardalis.ListStartupServices;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Mime;
using Ardalis.ListStartupServices;
using BlazorAdmin;
using BlazorAdmin.Services;
using Blazored.LocalStorage;
@@ -20,220 +26,212 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Mime;
namespace Microsoft.eShopWeb.Web
namespace Microsoft.eShopWeb.Web;
public class Startup
{
public class Startup
private IServiceCollection _services;
public Startup(IConfiguration configuration)
{
private IServiceCollection _services;
Configuration = configuration;
}
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public IConfiguration Configuration { get; }
public void ConfigureDevelopmentServices(IServiceCollection services)
{
// use in-memory database
ConfigureInMemoryDatabases(services);
public void ConfigureDevelopmentServices(IServiceCollection services)
{
// use in-memory database
ConfigureInMemoryDatabases(services);
// use real database
//ConfigureProductionServices(services);
}
// use real database
//ConfigureProductionServices(services);
}
public void ConfigureDockerServices(IServiceCollection services)
{
services.AddDataProtection()
.SetApplicationName("eshopwebmvc")
.PersistKeysToFileSystem(new DirectoryInfo(@"./"));
public void ConfigureDockerServices(IServiceCollection services)
{
services.AddDataProtection()
.SetApplicationName("eshopwebmvc")
.PersistKeysToFileSystem(new DirectoryInfo(@"./"));
ConfigureDevelopmentServices(services);
}
ConfigureDevelopmentServices(services);
}
private void ConfigureInMemoryDatabases(IServiceCollection services)
{
// use in-memory database
services.AddDbContext<CatalogContext>(c =>
c.UseInMemoryDatabase("Catalog"));
private void ConfigureInMemoryDatabases(IServiceCollection services)
{
// use in-memory database
services.AddDbContext<CatalogContext>(c =>
c.UseInMemoryDatabase("Catalog"));
// Add Identity DbContext
services.AddDbContext<AppIdentityDbContext>(options =>
options.UseInMemoryDatabase("Identity"));
// Add Identity DbContext
services.AddDbContext<AppIdentityDbContext>(options =>
options.UseInMemoryDatabase("Identity"));
ConfigureServices(services);
}
ConfigureServices(services);
}
public void ConfigureProductionServices(IServiceCollection services)
{
// use real database
// Requires LocalDB which can be installed with SQL Server Express 2016
// https://www.microsoft.com/en-us/download/details.aspx?id=54284
services.AddDbContext<CatalogContext>(c =>
c.UseSqlServer(Configuration.GetConnectionString("CatalogConnection")));
public void ConfigureProductionServices(IServiceCollection services)
{
// use real database
// Requires LocalDB which can be installed with SQL Server Express 2016
// https://www.microsoft.com/en-us/download/details.aspx?id=54284
services.AddDbContext<CatalogContext>(c =>
c.UseSqlServer(Configuration.GetConnectionString("CatalogConnection")));
// Add Identity DbContext
services.AddDbContext<AppIdentityDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("IdentityConnection")));
// Add Identity DbContext
services.AddDbContext<AppIdentityDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("IdentityConnection")));
ConfigureServices(services);
}
ConfigureServices(services);
}
public void ConfigureTestingServices(IServiceCollection services)
{
ConfigureInMemoryDatabases(services);
}
public void ConfigureTestingServices(IServiceCollection services)
{
ConfigureInMemoryDatabases(services);
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddCookieSettings();
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddCookieSettings();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Lax;
});
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddDefaultUI()
.AddEntityFrameworkStores<AppIdentityDbContext>()
.AddDefaultTokenProviders();
services.AddScoped<ITokenClaimsService, IdentityTokenClaimService>();
services.AddCoreServices(Configuration);
services.AddWebServices(Configuration);
// Add memory cache services
services.AddMemoryCache();
services.AddRouting(options =>
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Lax;
});
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddDefaultUI()
.AddEntityFrameworkStores<AppIdentityDbContext>()
.AddDefaultTokenProviders();
services.AddScoped<ITokenClaimsService, IdentityTokenClaimService>();
services.AddCoreServices(Configuration);
services.AddWebServices(Configuration);
// Add memory cache services
services.AddMemoryCache();
services.AddRouting(options =>
{
// Replace the type and the name used to refer to it with your own
// IOutboundParameterTransformer implementation
options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
});
services.AddMvc(options =>
{
options.Conventions.Add(new RouteTokenTransformerConvention(
new SlugifyParameterTransformer()));
});
services.AddControllersWithViews();
services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/Basket/Checkout");
});
services.AddHttpContextAccessor();
services.AddHealthChecks();
services.Configure<ServiceConfig>(config =>
{
config.Services = new List<ServiceDescriptor>(services);
config.Path = "/allservices";
});
var baseUrlConfig = new BaseUrlConfiguration();
Configuration.Bind(BaseUrlConfiguration.CONFIG_NAME, baseUrlConfig);
services.AddScoped<BaseUrlConfiguration>(sp => baseUrlConfig);
// Blazor Admin Required Services for Prerendering
services.AddScoped<HttpClient>(s => new HttpClient
{
BaseAddress = new Uri(baseUrlConfig.WebBase)
});
// add blazor services
services.AddBlazoredLocalStorage();
services.AddServerSideBlazor();
services.AddScoped<ToastService>();
services.AddScoped<HttpService>();
services.AddBlazorServices();
services.AddDatabaseDeveloperPageExceptionFilter();
_services = services; // used to debug registered services
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
});
services.AddMvc(options =>
{
var catalogBaseUrl = Configuration.GetValue(typeof(string), "CatalogBaseUrl") as string;
if (!string.IsNullOrEmpty(catalogBaseUrl))
{
app.Use((context, next) =>
{
context.Request.PathBase = new PathString(catalogBaseUrl);
return next();
});
}
options.Conventions.Add(new RouteTokenTransformerConvention(
new SlugifyParameterTransformer()));
app.UseHealthChecks("/health",
new HealthCheckOptions
{
ResponseWriter = async (context, report) =>
{
var result = new
{
status = report.Status.ToString(),
errors = report.Entries.Select(e => new
{
key = e.Key,
value = Enum.GetName(typeof(HealthStatus), e.Value.Status)
})
}.ToJson();
context.Response.ContentType = MediaTypeNames.Application.Json;
await context.Response.WriteAsync(result);
}
});
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseShowAllServicesMiddleware();
app.UseMigrationsEndPoint();
app.UseWebAssemblyDebugging();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
});
services.AddControllersWithViews();
services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/Basket/Checkout");
});
services.AddHttpContextAccessor();
services.AddHealthChecks();
services.Configure<ServiceConfig>(config =>
{
config.Services = new List<ServiceDescriptor>(services);
app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
config.Path = "/allservices";
});
app.UseCookiePolicy();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute("default", "{controller:slugify=Home}/{action:slugify=Index}/{id?}");
endpoints.MapRazorPages();
endpoints.MapHealthChecks("home_page_health_check");
endpoints.MapHealthChecks("api_health_check");
//endpoints.MapBlazorHub("/admin");
endpoints.MapFallbackToFile("index.html");
});
}
var baseUrlConfig = new BaseUrlConfiguration();
Configuration.Bind(BaseUrlConfiguration.CONFIG_NAME, baseUrlConfig);
services.AddScoped<BaseUrlConfiguration>(sp => baseUrlConfig);
// Blazor Admin Required Services for Prerendering
services.AddScoped<HttpClient>(s => new HttpClient
{
BaseAddress = new Uri(baseUrlConfig.WebBase)
});
// add blazor services
services.AddBlazoredLocalStorage();
services.AddServerSideBlazor();
services.AddScoped<ToastService>();
services.AddScoped<HttpService>();
services.AddBlazorServices();
services.AddDatabaseDeveloperPageExceptionFilter();
_services = services; // used to debug registered services
}
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
var catalogBaseUrl = Configuration.GetValue(typeof(string), "CatalogBaseUrl") as string;
if (!string.IsNullOrEmpty(catalogBaseUrl))
{
app.Use((context, next) =>
{
context.Request.PathBase = new PathString(catalogBaseUrl);
return next();
});
}
app.UseHealthChecks("/health",
new HealthCheckOptions
{
ResponseWriter = async (context, report) =>
{
var result = new
{
status = report.Status.ToString(),
errors = report.Entries.Select(e => new
{
key = e.Key,
value = Enum.GetName(typeof(HealthStatus), e.Value.Status)
})
}.ToJson();
context.Response.ContentType = MediaTypeNames.Application.Json;
await context.Response.WriteAsync(result);
}
});
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseShowAllServicesMiddleware();
app.UseMigrationsEndPoint();
app.UseWebAssemblyDebugging();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute("default", "{controller:slugify=Home}/{action:slugify=Index}/{id?}");
endpoints.MapRazorPages();
endpoints.MapHealthChecks("home_page_health_check");
endpoints.MapHealthChecks("api_health_check");
//endpoints.MapBlazorHub("/admin");
endpoints.MapFallbackToFile("index.html");
});
}
}

View File

@@ -1,18 +1,17 @@
using System.ComponentModel.DataAnnotations;
namespace Microsoft.eShopWeb.Web.ViewModels.Account
namespace Microsoft.eShopWeb.Web.ViewModels.Account;
public class LoginViewModel
{
public class LoginViewModel
{
[Required]
[EmailAddress]
public string Email { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
[Display(Name = "Remember me?")]
public bool RememberMe { get; set; }
}
[Display(Name = "Remember me?")]
public bool RememberMe { get; set; }
}

View File

@@ -1,18 +1,17 @@
using System.ComponentModel.DataAnnotations;
namespace Microsoft.eShopWeb.Web.ViewModels.Account
namespace Microsoft.eShopWeb.Web.ViewModels.Account;
public class LoginWith2faViewModel
{
public class LoginWith2faViewModel
{
[Required]
[StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Text)]
[Display(Name = "Authenticator code")]
public string TwoFactorCode { get; set; }
[Required]
[StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Text)]
[Display(Name = "Authenticator code")]
public string TwoFactorCode { get; set; }
[Display(Name = "Remember this machine")]
public bool RememberMachine { get; set; }
[Display(Name = "Remember this machine")]
public bool RememberMachine { get; set; }
public bool RememberMe { get; set; }
}
public bool RememberMe { get; set; }
}

View File

@@ -1,23 +1,22 @@
using System.ComponentModel.DataAnnotations;
namespace Microsoft.eShopWeb.Web.ViewModels.Account
namespace Microsoft.eShopWeb.Web.ViewModels.Account;
public class RegisterViewModel
{
public class RegisterViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; }
[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; }
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
}
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
}

View File

@@ -1,23 +1,22 @@
using System.ComponentModel.DataAnnotations;
namespace Microsoft.eShopWeb.Web.ViewModels.Account
namespace Microsoft.eShopWeb.Web.ViewModels.Account;
public class ResetPasswordViewModel
{
public class ResetPasswordViewModel
{
[Required]
[EmailAddress]
public string Email { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
public string Password { get; set; }
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
public string Password { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
public string Code { get; set; }
}
public string Code { get; set; }
}

View File

@@ -1,7 +1,6 @@
namespace Microsoft.eShopWeb.Web.ViewModels
namespace Microsoft.eShopWeb.Web.ViewModels;
public class BasketComponentViewModel
{
public class BasketComponentViewModel
{
public int ItemsCount { get; set; }
}
public int ItemsCount { get; set; }
}

View File

@@ -1,15 +1,14 @@
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace Microsoft.eShopWeb.Web.ViewModels
namespace Microsoft.eShopWeb.Web.ViewModels;
public class CatalogIndexViewModel
{
public class CatalogIndexViewModel
{
public List<CatalogItemViewModel> CatalogItems { get; set; }
public List<SelectListItem> Brands { get; set; }
public List<SelectListItem> Types { get; set; }
public int? BrandFilterApplied { get; set; }
public int? TypesFilterApplied { get; set; }
public PaginationInfoViewModel PaginationInfo { get; set; }
}
public List<CatalogItemViewModel> CatalogItems { get; set; }
public List<SelectListItem> Brands { get; set; }
public List<SelectListItem> Types { get; set; }
public int? BrandFilterApplied { get; set; }
public int? TypesFilterApplied { get; set; }
public PaginationInfoViewModel PaginationInfo { get; set; }
}

View File

@@ -1,10 +1,9 @@
namespace Microsoft.eShopWeb.Web.ViewModels
namespace Microsoft.eShopWeb.Web.ViewModels;
public class CatalogItemViewModel
{
public class CatalogItemViewModel
{
public int Id { get; set; }
public string Name { get; set; }
public string PictureUri { get; set; }
public decimal Price { get; set; }
}
public int Id { get; set; }
public string Name { get; set; }
public string PictureUri { get; set; }
public decimal Price { get; set; }
}

View File

@@ -1,9 +1,8 @@
namespace Microsoft.eShopWeb.Web.ViewModels.File
namespace Microsoft.eShopWeb.Web.ViewModels.File;
public class FileViewModel
{
public class FileViewModel
{
public string FileName { get; set; }
public string Url { get; set; }
public string DataBase64 { get; set; }
}
public string FileName { get; set; }
public string Url { get; set; }
public string DataBase64 { get; set; }
}

View File

@@ -1,25 +1,24 @@
using System.ComponentModel.DataAnnotations;
namespace Microsoft.eShopWeb.Web.ViewModels.Manage
namespace Microsoft.eShopWeb.Web.ViewModels.Manage;
public class ChangePasswordViewModel
{
public class ChangePasswordViewModel
{
[Required]
[DataType(DataType.Password)]
[Display(Name = "Current password")]
public string OldPassword { get; set; }
[Required]
[DataType(DataType.Password)]
[Display(Name = "Current password")]
public string OldPassword { get; set; }
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "New password")]
public string NewPassword { get; set; }
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "New password")]
public string NewPassword { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm new password")]
[Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm new password")]
[Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
public string StatusMessage { get; set; }
}
public string StatusMessage { get; set; }
}

View File

@@ -1,19 +1,18 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace Microsoft.eShopWeb.Web.ViewModels.Manage
namespace Microsoft.eShopWeb.Web.ViewModels.Manage;
public class EnableAuthenticatorViewModel
{
public class EnableAuthenticatorViewModel
{
[Required]
[StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Text)]
[Display(Name = "Verification Code")]
public string Code { get; set; }
[Required]
[StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Text)]
[Display(Name = "Verification Code")]
public string Code { get; set; }
[ReadOnly(true)]
public string SharedKey { get; set; }
[ReadOnly(true)]
public string SharedKey { get; set; }
public string AuthenticatorUri { get; set; }
}
public string AuthenticatorUri { get; set; }
}

View File

@@ -1,14 +1,13 @@
using Microsoft.AspNetCore.Authentication;
using System.Collections.Generic;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using System.Collections.Generic;
namespace Microsoft.eShopWeb.Web.ViewModels.Manage
namespace Microsoft.eShopWeb.Web.ViewModels.Manage;
public class ExternalLoginsViewModel
{
public class ExternalLoginsViewModel
{
public IList<UserLoginInfo> CurrentLogins { get; set; }
public IList<AuthenticationScheme> OtherLogins { get; set; }
public bool ShowRemoveButton { get; set; }
public string StatusMessage { get; set; }
}
public IList<UserLoginInfo> CurrentLogins { get; set; }
public IList<AuthenticationScheme> OtherLogins { get; set; }
public bool ShowRemoveButton { get; set; }
public string StatusMessage { get; set; }
}

View File

@@ -1,7 +1,6 @@
namespace Microsoft.eShopWeb.Web.ViewModels.Manage
namespace Microsoft.eShopWeb.Web.ViewModels.Manage;
public class GenerateRecoveryCodesViewModel
{
public class GenerateRecoveryCodesViewModel
{
public string[] RecoveryCodes { get; set; }
}
public string[] RecoveryCodes { get; set; }
}

View File

@@ -1,21 +1,20 @@
using System.ComponentModel.DataAnnotations;
namespace Microsoft.eShopWeb.Web.ViewModels.Manage
namespace Microsoft.eShopWeb.Web.ViewModels.Manage;
public class IndexViewModel
{
public class IndexViewModel
{
public string Username { get; set; }
public string Username { get; set; }
public bool IsEmailConfirmed { get; set; }
public bool IsEmailConfirmed { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
[Phone]
[Display(Name = "Phone number")]
public string PhoneNumber { get; set; }
[Phone]
[Display(Name = "Phone number")]
public string PhoneNumber { get; set; }
public string StatusMessage { get; set; }
}
public string StatusMessage { get; set; }
}

View File

@@ -1,8 +1,7 @@
namespace Microsoft.eShopWeb.Web.ViewModels.Manage
namespace Microsoft.eShopWeb.Web.ViewModels.Manage;
public class RemoveLoginViewModel
{
public class RemoveLoginViewModel
{
public string LoginProvider { get; set; }
public string ProviderKey { get; set; }
}
public string LoginProvider { get; set; }
public string ProviderKey { get; set; }
}

View File

@@ -1,20 +1,19 @@
using System.ComponentModel.DataAnnotations;
namespace Microsoft.eShopWeb.Web.ViewModels.Manage
namespace Microsoft.eShopWeb.Web.ViewModels.Manage;
public class SetPasswordViewModel
{
public class SetPasswordViewModel
{
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "New password")]
public string NewPassword { get; set; }
[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "New password")]
public string NewPassword { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm new password")]
[Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm new password")]
[Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
public string StatusMessage { get; set; }
}
public string StatusMessage { get; set; }
}

View File

@@ -1,9 +1,8 @@
namespace Microsoft.eShopWeb.Web.ViewModels.Manage
namespace Microsoft.eShopWeb.Web.ViewModels.Manage;
public class TwoFactorAuthenticationViewModel
{
public class TwoFactorAuthenticationViewModel
{
public bool HasAuthenticator { get; set; }
public int RecoveryCodesLeft { get; set; }
public bool Is2faEnabled { get; set; }
}
public bool HasAuthenticator { get; set; }
public int RecoveryCodesLeft { get; set; }
public bool Is2faEnabled { get; set; }
}

View File

@@ -1,12 +1,11 @@
namespace Microsoft.eShopWeb.Web.ViewModels
namespace Microsoft.eShopWeb.Web.ViewModels;
public class OrderItemViewModel
{
public class OrderItemViewModel
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public decimal UnitPrice { get; set; }
public decimal Discount => 0;
public int Units { get; set; }
public string PictureUrl { get; set; }
}
public int ProductId { get; set; }
public string ProductName { get; set; }
public decimal UnitPrice { get; set; }
public decimal Discount => 0;
public int Units { get; set; }
public string PictureUrl { get; set; }
}

View File

@@ -1,18 +1,17 @@
using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate;
using System;
using System;
using System.Collections.Generic;
using Microsoft.eShopWeb.ApplicationCore.Entities.OrderAggregate;
namespace Microsoft.eShopWeb.Web.ViewModels
namespace Microsoft.eShopWeb.Web.ViewModels;
public class OrderViewModel
{
public class OrderViewModel
{
private const string DEFAULT_STATUS = "Pending";
private const string DEFAULT_STATUS = "Pending";
public int OrderNumber { get; set; }
public DateTimeOffset OrderDate { get; set; }
public decimal Total { get; set; }
public string Status => DEFAULT_STATUS;
public Address ShippingAddress { get; set; }
public List<OrderItemViewModel> OrderItems { get; set; } = new List<OrderItemViewModel>();
}
public int OrderNumber { get; set; }
public DateTimeOffset OrderDate { get; set; }
public decimal Total { get; set; }
public string Status => DEFAULT_STATUS;
public Address ShippingAddress { get; set; }
public List<OrderItemViewModel> OrderItems { get; set; } = new List<OrderItemViewModel>();
}

View File

@@ -1,12 +1,11 @@
namespace Microsoft.eShopWeb.Web.ViewModels
namespace Microsoft.eShopWeb.Web.ViewModels;
public class PaginationInfoViewModel
{
public class PaginationInfoViewModel
{
public int TotalItems { get; set; }
public int ItemsPerPage { get; set; }
public int ActualPage { get; set; }
public int TotalPages { get; set; }
public string Previous { get; set; }
public string Next { get; set; }
}
public int TotalItems { get; set; }
public int ItemsPerPage { get; set; }
public int ActualPage { get; set; }
public int TotalPages { get; set; }
public string Previous { get; set; }
public string Next { get; set; }
}

View File

@@ -1,35 +1,34 @@
using Microsoft.AspNetCore.Mvc.Rendering;
using System;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using System;
namespace Microsoft.eShopWeb.Web.Views.Manage
namespace Microsoft.eShopWeb.Web.Views.Manage;
public static class ManageNavPages
{
public static class ManageNavPages
public static string ActivePageKey => "ActivePage";
public static string Index => "Index";
public static string ChangePassword => "ChangePassword";
public static string ExternalLogins => "ExternalLogins";
public static string TwoFactorAuthentication => "TwoFactorAuthentication";
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
public static string ChangePasswordNavClass(ViewContext viewContext) => PageNavClass(viewContext, ChangePassword);
public static string ExternalLoginsNavClass(ViewContext viewContext) => PageNavClass(viewContext, ExternalLogins);
public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication);
public static string PageNavClass(ViewContext viewContext, string page)
{
public static string ActivePageKey => "ActivePage";
public static string Index => "Index";
public static string ChangePassword => "ChangePassword";
public static string ExternalLogins => "ExternalLogins";
public static string TwoFactorAuthentication => "TwoFactorAuthentication";
public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index);
public static string ChangePasswordNavClass(ViewContext viewContext) => PageNavClass(viewContext, ChangePassword);
public static string ExternalLoginsNavClass(ViewContext viewContext) => PageNavClass(viewContext, ExternalLogins);
public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication);
public static string PageNavClass(ViewContext viewContext, string page)
{
var activePage = viewContext.ViewData["ActivePage"] as string;
return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : null;
}
public static void AddActivePage(this ViewDataDictionary viewData, string activePage) => viewData[ActivePageKey] = activePage;
var activePage = viewContext.ViewData["ActivePage"] as string;
return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : null;
}
public static void AddActivePage(this ViewDataDictionary viewData, string activePage) => viewData[ActivePageKey] = activePage;
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>Microsoft.eShopWeb.Web</RootNamespace>
<UserSecretsId>aspnet-Web2-1FA3F72E-E7E3-4360-9E49-1CCCD7FE85F7</UserSecretsId>
<LangVersion>latest</LangVersion>