From 39e0d5ec8b58713f9aa5621aa9439b524f5a2c83 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sun, 27 Feb 2022 16:15:35 -0500 Subject: [PATCH] Log on/off works; WIP on layout --- .../Extensions/WebLogUserExtensions.cs | 16 ++ src/MyWebLog.Data/WebLogDbContext.cs | 6 + .../Features/Admin/AdminController.cs | 19 +++ src/MyWebLog/Features/Admin/Index.cshtml | 5 + .../Features/Shared/_AdminLayout.cshtml | 48 ++++++ .../Features/Shared/_LogOnOffPartial.cshtml | 11 ++ src/MyWebLog/Features/Users/LogOn.cshtml | 30 ++++ src/MyWebLog/Features/Users/LogOnModel.cs | 24 +++ src/MyWebLog/Features/Users/UserController.cs | 51 ++++++- src/MyWebLog/Features/_ViewImports.cshtml | 6 + src/MyWebLog/GlobalUsings.cs | 1 + src/MyWebLog/MyWebLog.csproj | 17 ++- src/MyWebLog/Program.cs | 32 ++-- src/MyWebLog/Properties/Resources.Designer.cs | 117 +++++++++++++++ src/MyWebLog/Properties/Resources.resx | 138 ++++++++++++++++++ .../Default/Shared/_DefaultFooter.cshtml | 6 + .../Default/Shared/_DefaultHeader.cshtml | 23 +++ .../Themes/Default/Shared/_Layout.cshtml | 31 +++- src/MyWebLog/WebLogMiddleware.cs | 19 +-- src/MyWebLog/wwwroot/css/admin.css | 5 + src/MyWebLog/wwwroot/img/logo-dark.png | Bin 0 -> 3362 bytes src/MyWebLog/wwwroot/img/logo-light.png | Bin 0 -> 4135 bytes 22 files changed, 573 insertions(+), 32 deletions(-) create mode 100644 src/MyWebLog.Data/Extensions/WebLogUserExtensions.cs create mode 100644 src/MyWebLog/Features/Admin/AdminController.cs create mode 100644 src/MyWebLog/Features/Admin/Index.cshtml create mode 100644 src/MyWebLog/Features/Shared/_AdminLayout.cshtml create mode 100644 src/MyWebLog/Features/Shared/_LogOnOffPartial.cshtml create mode 100644 src/MyWebLog/Features/Users/LogOn.cshtml create mode 100644 src/MyWebLog/Features/Users/LogOnModel.cs create mode 100644 src/MyWebLog/Features/_ViewImports.cshtml create mode 100644 src/MyWebLog/Properties/Resources.Designer.cs create mode 100644 src/MyWebLog/Properties/Resources.resx create mode 100644 src/MyWebLog/Themes/Default/Shared/_DefaultFooter.cshtml create mode 100644 src/MyWebLog/Themes/Default/Shared/_DefaultHeader.cshtml create mode 100644 src/MyWebLog/wwwroot/css/admin.css create mode 100644 src/MyWebLog/wwwroot/img/logo-dark.png create mode 100644 src/MyWebLog/wwwroot/img/logo-light.png diff --git a/src/MyWebLog.Data/Extensions/WebLogUserExtensions.cs b/src/MyWebLog.Data/Extensions/WebLogUserExtensions.cs new file mode 100644 index 0000000..71d614e --- /dev/null +++ b/src/MyWebLog.Data/Extensions/WebLogUserExtensions.cs @@ -0,0 +1,16 @@ +using Microsoft.EntityFrameworkCore; + +namespace MyWebLog.Data; + +public static class WebLogUserExtensions +{ + /// + /// Find a user by their log on information (non-tracked) + /// + /// The user's e-mail address + /// The hash of the password provided by the user + /// The user, if the credentials match; null if they do not + public static async Task FindByEmail(this DbSet db, string email) => + await db.SingleOrDefaultAsync(wlu => wlu.UserName == email).ConfigureAwait(false); + +} diff --git a/src/MyWebLog.Data/WebLogDbContext.cs b/src/MyWebLog.Data/WebLogDbContext.cs index e4fcb92..3d63b97 100644 --- a/src/MyWebLog.Data/WebLogDbContext.cs +++ b/src/MyWebLog.Data/WebLogDbContext.cs @@ -56,6 +56,12 @@ public sealed class WebLogDbContext : DbContext /// Configuration options public WebLogDbContext(DbContextOptions options) : base(options) { } + /// + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); + } + /// protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/src/MyWebLog/Features/Admin/AdminController.cs b/src/MyWebLog/Features/Admin/AdminController.cs new file mode 100644 index 0000000..088ee6e --- /dev/null +++ b/src/MyWebLog/Features/Admin/AdminController.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Mvc; + +namespace MyWebLog.Features.Admin; + +/// +/// Controller for admin-specific displays and routes +/// +[Route("/admin")] +public class AdminController : MyWebLogController +{ + /// + public AdminController(WebLogDbContext db) : base(db) { } + + [HttpGet("")] + public IActionResult Index() + { + return View(); + } +} diff --git a/src/MyWebLog/Features/Admin/Index.cshtml b/src/MyWebLog/Features/Admin/Index.cshtml new file mode 100644 index 0000000..52fecf9 --- /dev/null +++ b/src/MyWebLog/Features/Admin/Index.cshtml @@ -0,0 +1,5 @@ +@{ + Layout = "_AdminLayout"; + ViewBag.Title = Resources.Dashboard; +} +

You're logged on!

diff --git a/src/MyWebLog/Features/Shared/_AdminLayout.cshtml b/src/MyWebLog/Features/Shared/_AdminLayout.cshtml new file mode 100644 index 0000000..d32e32f --- /dev/null +++ b/src/MyWebLog/Features/Shared/_AdminLayout.cshtml @@ -0,0 +1,48 @@ +@inject IHttpContextAccessor ctxAcc +@{ + var details = WebLogCache.Get(ctxAcc.HttpContext!); +} + + + + + @ViewBag.Title « @Resources.Admin « @details.Name + + + + +
+ +
+
+

@ViewBag.Title

+ @* Each.Messages + @Current.ToDisplay + @EndEach *@ + @RenderBody() +
+
+
+
+
myWebLog
+
+
+
+ + + diff --git a/src/MyWebLog/Features/Shared/_LogOnOffPartial.cshtml b/src/MyWebLog/Features/Shared/_LogOnOffPartial.cshtml new file mode 100644 index 0000000..c182a99 --- /dev/null +++ b/src/MyWebLog/Features/Shared/_LogOnOffPartial.cshtml @@ -0,0 +1,11 @@ + diff --git a/src/MyWebLog/Features/Users/LogOn.cshtml b/src/MyWebLog/Features/Users/LogOn.cshtml new file mode 100644 index 0000000..9a034e9 --- /dev/null +++ b/src/MyWebLog/Features/Users/LogOn.cshtml @@ -0,0 +1,30 @@ +@model LogOnModel +@{ + Layout = "_AdminLayout"; + ViewBag.Title = @Resources.LogOn; +} +
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+ +
+
+
+
+
diff --git a/src/MyWebLog/Features/Users/LogOnModel.cs b/src/MyWebLog/Features/Users/LogOnModel.cs new file mode 100644 index 0000000..b215ea8 --- /dev/null +++ b/src/MyWebLog/Features/Users/LogOnModel.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; + +namespace MyWebLog.Features.Users; + +/// +/// The model to use to allow a user to log on +/// +public class LogOnModel +{ + /// + /// The user's e-mail address + /// + [Required(AllowEmptyStrings = false)] + [EmailAddress] + [Display(ResourceType = typeof(Resources), Name = "EmailAddress")] + public string EmailAddress { get; set; } = ""; + + /// + /// The user's password + /// + [Required(AllowEmptyStrings = false)] + [Display(ResourceType = typeof(Resources), Name = "Password")] + public string Password { get; set; } = ""; +} diff --git a/src/MyWebLog/Features/Users/UserController.cs b/src/MyWebLog/Features/Users/UserController.cs index 7123c8a..5a2856c 100644 --- a/src/MyWebLog/Features/Users/UserController.cs +++ b/src/MyWebLog/Features/Users/UserController.cs @@ -1,4 +1,8 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; using System.Security.Cryptography; using System.Text; @@ -7,6 +11,7 @@ namespace MyWebLog.Features.Users; /// /// Controller for the users feature /// +[Route("/user")] public class UserController : MyWebLogController { /// @@ -19,15 +24,53 @@ public class UserController : MyWebLogController internal static string HashedPassword(string plainText, string email, Guid salt) { var allSalt = salt.ToByteArray().Concat(Encoding.UTF8.GetBytes(email)).ToArray(); - using var alg = new Rfc2898DeriveBytes(plainText, allSalt, 2_048); + using Rfc2898DeriveBytes alg = new(plainText, allSalt, 2_048); return Convert.ToBase64String(alg.GetBytes(64)); } /// public UserController(WebLogDbContext db) : base(db) { } - public IActionResult Index() + [HttpGet("log-on")] + public IActionResult LogOn() => + View(new LogOnModel()); + + [HttpPost("log-on")] + public async Task DoLogOn(LogOnModel model) { - return View(); + var user = await Db.Users.FindByEmail(model.EmailAddress); + + if (user == null || user.PasswordHash != HashedPassword(model.Password, user.UserName, user.Salt)) + { + // TODO: make error, not 404 + return NotFound(); + } + + List claims = new() + { + new(ClaimTypes.NameIdentifier, user.Id), + new(ClaimTypes.Name, $"{user.FirstName} {user.LastName}"), + new(ClaimTypes.GivenName, user.PreferredName), + new(ClaimTypes.Role, user.AuthorizationLevel.ToString()) + }; + ClaimsIdentity identity = new(claims, CookieAuthenticationDefaults.AuthenticationScheme); + + await HttpContext.SignInAsync(identity.AuthenticationType, new(identity), + new() { IssuedUtc = DateTime.UtcNow }); + + // TODO: confirmation message + + return RedirectToAction("Index", "Admin"); + } + + [HttpGet("log-off")] + [Authorize] + public async Task LogOff() + { + await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + + // TODO: confirmation message + + return LocalRedirect("~/"); } } diff --git a/src/MyWebLog/Features/_ViewImports.cshtml b/src/MyWebLog/Features/_ViewImports.cshtml new file mode 100644 index 0000000..65c93a1 --- /dev/null +++ b/src/MyWebLog/Features/_ViewImports.cshtml @@ -0,0 +1,6 @@ +@namespace MyWebLog.Features + +@using MyWebLog +@using MyWebLog.Properties + +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/src/MyWebLog/GlobalUsings.cs b/src/MyWebLog/GlobalUsings.cs index f3f143e..45f4200 100644 --- a/src/MyWebLog/GlobalUsings.cs +++ b/src/MyWebLog/GlobalUsings.cs @@ -1,2 +1,3 @@ global using MyWebLog.Data; global using MyWebLog.Features.Shared; +global using MyWebLog.Properties; diff --git a/src/MyWebLog/MyWebLog.csproj b/src/MyWebLog/MyWebLog.csproj index c0b846f..0f4f91a 100644 --- a/src/MyWebLog/MyWebLog.csproj +++ b/src/MyWebLog/MyWebLog.csproj @@ -7,7 +7,7 @@ - + @@ -21,4 +21,19 @@ + + + True + True + Resources.resx + + + + + + PublicResXFileCodeGenerator + Resources.Designer.cs + + + diff --git a/src/MyWebLog/Program.cs b/src/MyWebLog/Program.cs index 6f3b234..2a16434 100644 --- a/src/MyWebLog/Program.cs +++ b/src/MyWebLog/Program.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using MyWebLog; using MyWebLog.Features; @@ -12,20 +13,23 @@ if (args.Length > 0 && args[0] == "init") var builder = WebApplication.CreateBuilder(args); -builder.Services.AddMvc(opts => opts.Conventions.Add(new FeatureControllerModelConvention())) - .AddRazorOptions(opts => - { - opts.ViewLocationFormats.Clear(); - opts.ViewLocationFormats.Add("/Themes/{3}/{0}.cshtml"); - opts.ViewLocationFormats.Add("/Themes/{3}/Shared/{0}.cshtml"); - opts.ViewLocationFormats.Add("/Themes/Default/{0}.cshtml"); - opts.ViewLocationFormats.Add("/Themes/Default/Shared/{0}.cshtml"); - opts.ViewLocationFormats.Add("/Features/{2}/{1}/{0}.cshtml"); - opts.ViewLocationFormats.Add("/Features/{2}/{0}.cshtml"); - opts.ViewLocationFormats.Add("/Features/Shared/{0}.cshtml"); - opts.ViewLocationExpanders.Add(new FeatureViewLocationExpander()); - opts.ViewLocationExpanders.Add(new ThemeViewLocationExpander()); - }); +builder.Services.AddMvc(opts => +{ + opts.Conventions.Add(new FeatureControllerModelConvention()); + opts.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); +}).AddRazorOptions(opts => +{ + opts.ViewLocationFormats.Clear(); + opts.ViewLocationFormats.Add("/Themes/{3}/{0}.cshtml"); + opts.ViewLocationFormats.Add("/Themes/{3}/Shared/{0}.cshtml"); + opts.ViewLocationFormats.Add("/Themes/Default/{0}.cshtml"); + opts.ViewLocationFormats.Add("/Themes/Default/Shared/{0}.cshtml"); + opts.ViewLocationFormats.Add("/Features/{2}/{1}/{0}.cshtml"); + opts.ViewLocationFormats.Add("/Features/{2}/{0}.cshtml"); + opts.ViewLocationFormats.Add("/Features/Shared/{0}.cshtml"); + opts.ViewLocationExpanders.Add(new FeatureViewLocationExpander()); + opts.ViewLocationExpanders.Add(new ThemeViewLocationExpander()); +}); builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(opts => { diff --git a/src/MyWebLog/Properties/Resources.Designer.cs b/src/MyWebLog/Properties/Resources.Designer.cs new file mode 100644 index 0000000..a087989 --- /dev/null +++ b/src/MyWebLog/Properties/Resources.Designer.cs @@ -0,0 +1,117 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace MyWebLog.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MyWebLog.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Admin. + /// + public static string Admin { + get { + return ResourceManager.GetString("Admin", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Dashboard. + /// + public static string Dashboard { + get { + return ResourceManager.GetString("Dashboard", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to E-mail Address. + /// + public static string EmailAddress { + get { + return ResourceManager.GetString("EmailAddress", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Log Off. + /// + public static string LogOff { + get { + return ResourceManager.GetString("LogOff", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Log On. + /// + public static string LogOn { + get { + return ResourceManager.GetString("LogOn", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Password. + /// + public static string Password { + get { + return ResourceManager.GetString("Password", resourceCulture); + } + } + } +} diff --git a/src/MyWebLog/Properties/Resources.resx b/src/MyWebLog/Properties/Resources.resx new file mode 100644 index 0000000..8a5e1ba --- /dev/null +++ b/src/MyWebLog/Properties/Resources.resx @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Admin + + + Dashboard + + + E-mail Address + + + Log Off + + + Log On + + + Password + + \ No newline at end of file diff --git a/src/MyWebLog/Themes/Default/Shared/_DefaultFooter.cshtml b/src/MyWebLog/Themes/Default/Shared/_DefaultFooter.cshtml new file mode 100644 index 0000000..b9f86bb --- /dev/null +++ b/src/MyWebLog/Themes/Default/Shared/_DefaultFooter.cshtml @@ -0,0 +1,6 @@ +
+
+
+ myWebLog +
+
diff --git a/src/MyWebLog/Themes/Default/Shared/_DefaultHeader.cshtml b/src/MyWebLog/Themes/Default/Shared/_DefaultHeader.cshtml new file mode 100644 index 0000000..6a958c4 --- /dev/null +++ b/src/MyWebLog/Themes/Default/Shared/_DefaultHeader.cshtml @@ -0,0 +1,23 @@ +@inject IHttpContextAccessor ctxAcc +@{ + var details = WebLogCache.Get(ctxAcc.HttpContext!); +} +
+ +
diff --git a/src/MyWebLog/Themes/Default/Shared/_Layout.cshtml b/src/MyWebLog/Themes/Default/Shared/_Layout.cshtml index 52c9886..bc16fbe 100644 --- a/src/MyWebLog/Themes/Default/Shared/_Layout.cshtml +++ b/src/MyWebLog/Themes/Default/Shared/_Layout.cshtml @@ -1,14 +1,37 @@ @inject IHttpContextAccessor ctxAcc +@{ + var details = WebLogCache.Get(ctxAcc.HttpContext!); +} - - @ViewBag.Title « @WebLogCache.Get(ctxAcc.HttpContext!).Name + + + + @await RenderSectionAsync("Style", false) + @ViewBag.Title « @details.Name -
+ @if (IsSectionDefined("Header")) + { + @await RenderSectionAsync("Header") + } + else + { + @await Html.PartialAsync("_DefaultHeader") + } +
@RenderBody() -
+ + @if (IsSectionDefined("Footer")) + { + @await RenderSectionAsync("Footer") + } else + { + @await Html.PartialAsync("_DefaultFooter") + } + @await RenderSectionAsync("Script", false) diff --git a/src/MyWebLog/WebLogMiddleware.cs b/src/MyWebLog/WebLogMiddleware.cs index c285b79..bc24d0e 100644 --- a/src/MyWebLog/WebLogMiddleware.cs +++ b/src/MyWebLog/WebLogMiddleware.cs @@ -73,17 +73,18 @@ public class WebLogMiddleware { var host = WebLogCache.HostToDb(context); - if (WebLogCache.Exists(host)) return; - - var db = context.RequestServices.GetRequiredService(); - var details = await db.WebLogDetails.FindByHost(context.Request.Host.ToUriComponent()); - if (details == null) + if (!WebLogCache.Exists(host)) { - context.Response.StatusCode = 404; - return; + var db = context.RequestServices.GetRequiredService(); + var details = await db.WebLogDetails.FindByHost(context.Request.Host.ToUriComponent()); + if (details == null) + { + context.Response.StatusCode = 404; + return; + } + + WebLogCache.Set(host, details); } - - WebLogCache.Set(host, details); await _next.Invoke(context); } diff --git a/src/MyWebLog/wwwroot/css/admin.css b/src/MyWebLog/wwwroot/css/admin.css new file mode 100644 index 0000000..f720fda --- /dev/null +++ b/src/MyWebLog/wwwroot/css/admin.css @@ -0,0 +1,5 @@ +footer { + background-color: #808080; + border-top: solid 1px black; + color: white; +} diff --git a/src/MyWebLog/wwwroot/img/logo-dark.png b/src/MyWebLog/wwwroot/img/logo-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..19bdcca2af3095d7f18345a28fbf9c6da0363daa GIT binary patch literal 3362 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 l1PBlyK!5-N0tD!0_<#2