diff --git a/src/Web/Areas/Identity/IdentityHostingStartup.cs b/src/Web/Areas/Identity/IdentityHostingStartup.cs new file mode 100644 index 0000000..57a55bd --- /dev/null +++ b/src/Web/Areas/Identity/IdentityHostingStartup.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI; +using Microsoft.EntityFrameworkCore; +using Microsoft.eShopWeb.Infrastructure.Identity; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +[assembly: HostingStartup(typeof(Microsoft.eShopWeb.Web.Areas.Identity.IdentityHostingStartup))] +namespace Microsoft.eShopWeb.Web.Areas.Identity +{ + public class IdentityHostingStartup : IHostingStartup + { + public void Configure(IWebHostBuilder builder) + { + builder.ConfigureServices((context, services) => { + }); + } + } +} \ No newline at end of file diff --git a/src/Web/Areas/Identity/Pages/Account/Login.cshtml b/src/Web/Areas/Identity/Pages/Account/Login.cshtml new file mode 100644 index 0000000..97c524d --- /dev/null +++ b/src/Web/Areas/Identity/Pages/Account/Login.cshtml @@ -0,0 +1,63 @@ +@page +@model LoginModel + +@{ + ViewData["Title"] = "Log in"; +} + + +
+

@ViewData["Title"]

+
+
+
+
+
+
+
+ + + +
+
+ + + +
+
+
+ +
+
+
+ +
+ +

+ Note that for demo purposes you don't need to register and can login with these credentials: +

+

+ User: demouser@microsoft.com +

+

+ Password: Pass@word1 +

+
+
+
+
+
+ +@section Scripts { + +} diff --git a/src/Web/Areas/Identity/Pages/Account/Login.cshtml.cs b/src/Web/Areas/Identity/Pages/Account/Login.cshtml.cs new file mode 100644 index 0000000..72ecf94 --- /dev/null +++ b/src/Web/Areas/Identity/Pages/Account/Login.cshtml.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.eShopWeb.Infrastructure.Identity; +using Microsoft.Extensions.Logging; + +namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account +{ + [AllowAnonymous] + public class LoginModel : PageModel + { + private readonly SignInManager _signInManager; + private readonly ILogger _logger; + + public LoginModel(SignInManager signInManager, ILogger logger) + { + _signInManager = signInManager; + _logger = logger; + } + + [BindProperty] + public InputModel Input { get; set; } + + public IList 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)) + { + ModelState.AddModelError(string.Empty, ErrorMessage); + } + + 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 OnPostAsync(string returnUrl = null) + { + returnUrl = returnUrl ?? Url.Content("~/"); + + if (ModelState.IsValid) + { + // 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); + if (result.Succeeded) + { + _logger.LogInformation("User logged in."); + 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(); + } + } +} diff --git a/src/Web/Areas/Identity/Pages/Account/_ViewImports.cshtml b/src/Web/Areas/Identity/Pages/Account/_ViewImports.cshtml new file mode 100644 index 0000000..65840eb --- /dev/null +++ b/src/Web/Areas/Identity/Pages/Account/_ViewImports.cshtml @@ -0,0 +1 @@ +@using Microsoft.eShopWeb.Web.Areas.Identity.Pages.Account \ No newline at end of file diff --git a/src/Web/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml b/src/Web/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml new file mode 100644 index 0000000..bacc0ae --- /dev/null +++ b/src/Web/Areas/Identity/Pages/_ValidationScriptsPartial.cshtml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/src/Web/Areas/Identity/Pages/_ViewImports.cshtml b/src/Web/Areas/Identity/Pages/_ViewImports.cshtml new file mode 100644 index 0000000..82c4f63 --- /dev/null +++ b/src/Web/Areas/Identity/Pages/_ViewImports.cshtml @@ -0,0 +1,5 @@ +@using Microsoft.AspNetCore.Identity +@using Microsoft.eShopWeb.Web.Areas.Identity +@using Microsoft.eShopWeb.Infrastructure.Identity +@namespace Microsoft.eShopWeb.Web.Areas.Identity.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/src/Web/Startup.cs b/src/Web/Startup.cs index f35bd23..93d399a 100644 --- a/src/Web/Startup.cs +++ b/src/Web/Startup.cs @@ -114,11 +114,13 @@ namespace Microsoft.eShopWeb.Web { options.Conventions.Add(new RouteTokenTransformerConvention( new SlugifyParameterTransformer())); + } ) .AddRazorPagesOptions(options => { options.Conventions.AuthorizePage("/Basket/Checkout"); + options.AllowAreas = true; }) .SetCompatibilityVersion(CompatibilityVersion.Version_2_2); @@ -164,7 +166,6 @@ namespace Microsoft.eShopWeb.Web { options.Cookie.HttpOnly = true; options.ExpireTimeSpan = TimeSpan.FromHours(1); - options.LoginPath = "/Account/Signin"; options.LogoutPath = "/Account/Signout"; options.Cookie = new CookieBuilder { diff --git a/src/Web/Views/Account/Signin.cshtml b/src/Web/Views/Account/Signin.cshtml deleted file mode 100644 index 9d1742d..0000000 --- a/src/Web/Views/Account/Signin.cshtml +++ /dev/null @@ -1,59 +0,0 @@ -@model LoginViewModel -@{ - ViewData["Title"] = "Log in"; -} -
- -
- - -@section Scripts { - @{ await Html.RenderPartialAsync("_ValidationScriptsPartial"); } -} diff --git a/src/Web/Views/Shared/_Layout.cshtml b/src/Web/Views/Shared/_Layout.cshtml index 00ea90a..cd4ae24 100644 --- a/src/Web/Views/Shared/_Layout.cshtml +++ b/src/Web/Views/Shared/_Layout.cshtml @@ -24,29 +24,31 @@ -
-
- -
-
- @RenderBody() -
-
-
-
-
- -
-
-
-
+
+
+
+ +
+
+ @RenderBody() +
+
+
+
+
+ +
+
+
+
+
diff --git a/src/Web/Views/Shared/_LoginPartial.cshtml b/src/Web/Views/Shared/_LoginPartial.cshtml index fce87e4..1f454f6 100644 --- a/src/Web/Views/Shared/_LoginPartial.cshtml +++ b/src/Web/Views/Shared/_LoginPartial.cshtml @@ -40,7 +40,7 @@ else
diff --git a/src/Web/Web.csproj b/src/Web/Web.csproj index c27268f..73d1dbe 100644 --- a/src/Web/Web.csproj +++ b/src/Web/Web.csproj @@ -18,6 +18,7 @@ + @@ -74,7 +75,6 @@ <_ContentIncludedByDefault Remove="Views\Account\Lockout.cshtml" /> <_ContentIncludedByDefault Remove="Views\Account\LoginWith2fa.cshtml" /> <_ContentIncludedByDefault Remove="Views\Account\Register.cshtml" /> - <_ContentIncludedByDefault Remove="Views\Account\Signin.cshtml" /> <_ContentIncludedByDefault Remove="Views\Manage\ChangePassword.cshtml" /> <_ContentIncludedByDefault Remove="Views\Manage\Disable2fa.cshtml" /> <_ContentIncludedByDefault Remove="Views\Manage\EnableAuthenticator.cshtml" /> @@ -96,9 +96,6 @@ - - - diff --git a/src/Web/wwwroot/css/app.component.css b/src/Web/wwwroot/css/app.component.css index 7b808bb..802c163 100644 --- a/src/Web/wwwroot/css/app.component.css +++ b/src/Web/wwwroot/css/app.component.css @@ -4,10 +4,17 @@ margin-top: 2.5rem; padding-bottom: 2.5rem; padding-top: 2.5rem; - width: 100%; } + width: 100%; + bottom: 0; } .esh-app-footer-brand { height: 50px; width: 230px; } .esh-app-header { margin: 15px; } + +.esh-app-wrapper { + display: flex; + min-height: 100vh; + flex-direction: column; + justify-content: space-between; } diff --git a/src/Web/wwwroot/css/app.component.min.css b/src/Web/wwwroot/css/app.component.min.css index 025695c..2841619 100644 --- a/src/Web/wwwroot/css/app.component.min.css +++ b/src/Web/wwwroot/css/app.component.min.css @@ -1 +1 @@ -.esh-app-footer{background-color:#000;border-top:1px solid #eee;margin-top:2.5rem;padding-bottom:2.5rem;padding-top:2.5rem;width:100%;}.esh-app-footer-brand{height:50px;width:230px;}.esh-app-header{margin:15px;} \ No newline at end of file +.esh-app-footer{background-color:#000;border-top:1px solid #eee;margin-top:2.5rem;padding-bottom:2.5rem;padding-top:2.5rem;width:100%;bottom:0;}.esh-app-footer-brand{height:50px;width:230px;}.esh-app-header{margin:15px;}.esh-app-wrapper{display:flex;min-height:100vh;flex-direction:column;justify-content:space-between;} \ No newline at end of file diff --git a/src/Web/wwwroot/css/app.component.scss b/src/Web/wwwroot/css/app.component.scss index 6e391df..be55675 100644 --- a/src/Web/wwwroot/css/app.component.scss +++ b/src/Web/wwwroot/css/app.component.scss @@ -11,17 +11,23 @@ padding-bottom: $padding; padding-top: $padding; width: 100%; - + bottom: 0; $height: 50px; &-brand { height: $height; width: 230px; } - } &-header { margin: 15px; } + + &-wrapper { + display: flex; + min-height: 100vh; + flex-direction: column; + justify-content: space-between + } } \ No newline at end of file