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 0000000..19bdcca Binary files /dev/null and b/src/MyWebLog/wwwroot/img/logo-dark.png differ diff --git a/src/MyWebLog/wwwroot/img/logo-light.png b/src/MyWebLog/wwwroot/img/logo-light.png new file mode 100644 index 0000000..c2d3357 Binary files /dev/null and b/src/MyWebLog/wwwroot/img/logo-light.png differ