From 5565be07f8d8d473759315fd99747c64e2ce3450 Mon Sep 17 00:00:00 2001 From: Jake Mannens Date: Fri, 15 Sep 2023 10:31:20 +1000 Subject: Completed initial login functionality --- App.razor | 30 +++++++++----- Controllers/LoginController.cs | 50 +++++++++++++++++++++++ HBContext.cs | 25 +++++++++--- MainLayout.razor | 22 +--------- MainLayout.razor.css | 53 +----------------------- Media.cs | 2 +- Pages/Component/RedirectLogin.razor | 6 +++ Pages/Component/Titlebar.razor | 77 +++++++++++++++++++++++++++++++++++ Pages/Component/Titlebar.razor.css | 79 ++++++++++++++++++++++++++++++++++++ Pages/Gallery.razor | 1 + Pages/Login.razor | 17 ++++++++ Pages/Login.razor.css | 6 +++ Pages/TagDefinitions.razor | 1 + Pages/Upload.razor | 1 + Pages/ViewMedia.razor | 1 + Program.cs | 6 ++- Services/UserService.cs | 42 ++++++++++++++++++- User.cs | 9 ++++ _Imports.razor | 2 + wwwroot/images/loginbg.webp | Bin 0 -> 2247672 bytes wwwroot/js/keyboard.js | 4 +- wwwroot/loginbg.webm | Bin 0 -> 390877 bytes 22 files changed, 340 insertions(+), 94 deletions(-) create mode 100644 Controllers/LoginController.cs create mode 100644 Pages/Component/RedirectLogin.razor create mode 100644 Pages/Component/Titlebar.razor create mode 100644 Pages/Component/Titlebar.razor.css create mode 100644 Pages/Login.razor create mode 100644 Pages/Login.razor.css create mode 100644 User.cs create mode 100644 wwwroot/images/loginbg.webp create mode 100644 wwwroot/loginbg.webm diff --git a/App.razor b/App.razor index d414dad..b4e47c9 100644 --- a/App.razor +++ b/App.razor @@ -1,11 +1,19 @@ - - - - - - Not found - -

Sorry, there's nothing at this address.

-
-
-
+ + + + + + + + + + + Not found + +

Sorry, there's nothing at this address.

+
+
+
+
diff --git a/Controllers/LoginController.cs b/Controllers/LoginController.cs new file mode 100644 index 0000000..b01553c --- /dev/null +++ b/Controllers/LoginController.cs @@ -0,0 +1,50 @@ +using HyperBooru.Services; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Cryptography.KeyDerivation; +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; + +namespace HyperBooru.Controllers; + +[ApiController] +[Route("/")] +public class LoginController : Controller { + private IHttpContextAccessor httpContextAccessor; + + public LoginController(IHttpContextAccessor httpContextAccessor) => + this.httpContextAccessor = httpContextAccessor; + + [HttpPost("Login")] + public async Task Login( + [FromForm] string username, + [FromForm] string password, + HBContext db) { + + var user = db.Users.FirstOrDefault(u => u.Username == username); + if(user is null) + return StatusCode(403); + + var hash = UserService.HashPassword(password); + if(hash != user.PasswordHash) + return StatusCode(403); + + var claims = new Claim[] { + new Claim(ClaimTypes.Name, user.Username), + new Claim("ObjectId", user.ObjectId.ToString()) + }; + + var claimsIdentity = new ClaimsIdentity( + claims, + CookieAuthenticationDefaults.AuthenticationScheme); + + var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); + + await httpContextAccessor.HttpContext!.SignInAsync(claimsPrincipal); + return Ok(); + } + + [HttpPost("Logout")] + public async Task Logout() => + await httpContextAccessor.HttpContext!.SignOutAsync(); +} diff --git a/HBContext.cs b/HBContext.cs index 415b745..bb0572f 100644 --- a/HBContext.cs +++ b/HBContext.cs @@ -3,14 +3,19 @@ using HyperBooru.Services; namespace HyperBooru; -public class HBContext : DbContext { - public const int NsfwTagId = -1; - public const int IngestTagId = -2; +enum HBObjectId { + NsfwTag = -1, + IngestTag = -2, + AdminUser = -3 +} +public class HBContext : DbContext { public static readonly Guid NsfwTag = new("EBDAD4F8-455A-4351-8017-1D4854D6FA38"); public static readonly Guid IngestTag = new("EA212801-5BCC-4C0E-814F-FB9D30DB58BC"); + public static readonly Guid AdminUser = new("4FA948F4-7C45-4F81-BB6B-E417491E6C96"); public DbSet Objects { get; set; } + public DbSet Users { get; set; } public DbSet TagDefinitions { get; set; } public DbSet Tags { get; set; } public DbSet Media { get; set; } @@ -42,19 +47,29 @@ public class HBContext : DbContext { // These should NEVER change modelBuilder.Entity().HasData(new TagDefinition[] { new() { - ObjectId = NsfwTagId, + ObjectId = (int) HBObjectId.NsfwTag, Guid = NsfwTag, Source = TagSource.Internal, Name = "nsfw" }, new() { - ObjectId = IngestTagId, + ObjectId = (int) HBObjectId.IngestTag, Guid = IngestTag, Source = TagSource.Internal, Name = "ingest" } }); + // Seed initial admin user + modelBuilder.Entity().HasData(new User[] { + new() { + ObjectId = (int) HBObjectId.AdminUser, + Guid = AdminUser, + Username = "admin", + PasswordHash = UserService.HashPassword("admin") + } + }); + // Some complex relationships cannot be inferred and require // additional configuration, as seen below. modelBuilder.Entity() diff --git a/MainLayout.razor b/MainLayout.razor index 2d3cadc..5d68b65 100644 --- a/MainLayout.razor +++ b/MainLayout.razor @@ -2,28 +2,8 @@ - +
@Body
- - - -@code { - private AboutDialog aboutDialog; -} diff --git a/MainLayout.razor.css b/MainLayout.razor.css index b0dea4e..0ce15c2 100644 --- a/MainLayout.razor.css +++ b/MainLayout.razor.css @@ -1,55 +1,4 @@ -div#navbar { - background: var(--col-navbar-bg); - box-shadow: rgba(0, 0, 0, 0.5) 0px 10px 10px; - display: flex; - z-index: 100; -} - -div#navbar > a { - color: white; - display: inline-block; - padding: 20px 20px 20px 20px; -} - -div#navbar > a:hover { - background: rgba(255, 255, 255, 0.4); - filter: none; -} - -div#navbar > a:active { - background: #fff; - color: var(--col-navbar-bg); -} - -p#nsfw-label { - align-self: center; - font-size: 9pt; - margin-left: auto; -} - -div#nsfw-switch { - align-self: center; - margin-left: 10px; -} - -div#navbar form { - display: flex; - margin: 0 20px 0 20px; - min-width: 30%; -} - -div#navbar input[type="text"] { - align-self: center; - background: var(--col-bg); - border-radius: 0; - color: white; - font-size: 12pt; - height: 40px !important; - margin: 0; - width: 100%; -} - -#content { +#content { flex: 1 1 calc(100vh - 59px); overflow-x: hidden; overflow-y: auto; diff --git a/Media.cs b/Media.cs index c3dfd31..dd7534c 100644 --- a/Media.cs +++ b/Media.cs @@ -15,7 +15,7 @@ public class Media : HBObject { public bool IsIngest => Tags .Select(t => t.TagDefinitionId) - .Contains(HBContext.IngestTagId); + .Contains((int) HBObjectId.IngestTag); public string? DisplayName { get { diff --git a/Pages/Component/RedirectLogin.razor b/Pages/Component/RedirectLogin.razor new file mode 100644 index 0000000..290a7ac --- /dev/null +++ b/Pages/Component/RedirectLogin.razor @@ -0,0 +1,6 @@ +@inject NavigationManager navigationManager + +@code { + protected override void OnInitialized() => + navigationManager.NavigateTo("/Login", true); +} \ No newline at end of file diff --git a/Pages/Component/Titlebar.razor b/Pages/Component/Titlebar.razor new file mode 100644 index 0000000..8033413 --- /dev/null +++ b/Pages/Component/Titlebar.razor @@ -0,0 +1,77 @@ +@inject IJSRuntime jsRuntime +@inject IUserService userService + + + + + + + + + + + + + +@code { + private AboutDialog aboutDialog; +} diff --git a/Pages/Component/Titlebar.razor.css b/Pages/Component/Titlebar.razor.css new file mode 100644 index 0000000..ea10740 --- /dev/null +++ b/Pages/Component/Titlebar.razor.css @@ -0,0 +1,79 @@ +div#navbar { + align-items: center; + background: var(--col-navbar-bg); + box-shadow: rgba(0, 0, 0, 0.5) 0px 10px 10px; + display: flex; + height: 59px; + z-index: 100; +} + +div#navbar > h2 { + margin-left: 20px; +} + +div#navbar > a { + align-items: center; + color: white; + display: flex; + height: 100%; + padding: 0 20px 0 20px; +} + +div#navbar > a:hover { + background: rgba(255, 255, 255, 0.4); + filter: none; +} + +div#navbar > a:active { + background: #fff; + color: var(--col-navbar-bg); +} + +p#nsfw-label { + align-self: center; + font-size: 9pt; + margin-left: auto; +} + +div#nsfw-switch { + align-self: center; + margin-left: 10px; +} + +form { + display: flex; + margin: 0 20px 0 20px; + min-width: 30%; +} + +form.login { + margin-left: auto; +} + +form.login.bad-login { + animation-iteration-count: 3; + animation-timing-function: linear; + animation: bad-login 0.2s; +} + +@keyframes bad-login { + 0% { transform: translateX(0); } + 33% { transform: translateX(-20px); } + 66% { transform: translateX(+20px); } + 100% { transform: translateX(0); } +} + +input[type="text"], input[type="password"] { + align-self: center; + background: var(--col-bg); + border-radius: 0; + color: white; + font-size: 12pt; + height: 40px !important; + margin: 0; + width: 100%; +} + +input[type="password"] { + margin-left: 20px; +} diff --git a/Pages/Gallery.razor b/Pages/Gallery.razor index 7e183f2..d473c28 100644 --- a/Pages/Gallery.razor +++ b/Pages/Gallery.razor @@ -6,6 +6,7 @@ @inject IUserService userService @inject IJSRuntime jsRuntime @implements IDisposable +@attribute [Authorize] @Title diff --git a/Pages/Login.razor b/Pages/Login.razor new file mode 100644 index 0000000..723a78a --- /dev/null +++ b/Pages/Login.razor @@ -0,0 +1,17 @@ +@page "/Login" +@inject NavigationManager navigationManager + +HyperBooru Login + +
+ +@code { + [CascadingParameter] + public Task AuthenticationState{ get; set; } + + protected override void OnInitialized() { + var authState = AuthenticationState.GetAwaiter().GetResult(); + if(authState!.User.Identity?.IsAuthenticated ?? false) + navigationManager.NavigateTo("/"); + } +} \ No newline at end of file diff --git a/Pages/Login.razor.css b/Pages/Login.razor.css new file mode 100644 index 0000000..fc8c8ca --- /dev/null +++ b/Pages/Login.razor.css @@ -0,0 +1,6 @@ +div { + background: url('/images/loginbg.webp'); + filter: brightness(0.6); + height: 100%; + width: 100%; +} \ No newline at end of file diff --git a/Pages/TagDefinitions.razor b/Pages/TagDefinitions.razor index 1a29b40..f728631 100644 --- a/Pages/TagDefinitions.razor +++ b/Pages/TagDefinitions.razor @@ -3,6 +3,7 @@ @inject ITagService tagService @inject IUserService userService @implements IDisposable +@attribute [Authorize] Tag Definitions diff --git a/Pages/Upload.razor b/Pages/Upload.razor index 7f7980b..614cec0 100644 --- a/Pages/Upload.razor +++ b/Pages/Upload.razor @@ -1,4 +1,5 @@ @page "/Upload" +@attribute [Authorize]

Drag a file to upload it
or click to select one or more file(s)

diff --git a/Pages/ViewMedia.razor b/Pages/ViewMedia.razor index 7823c75..444fbc5 100644 --- a/Pages/ViewMedia.razor +++ b/Pages/ViewMedia.razor @@ -4,6 +4,7 @@ @inject IDbContextFactory dbFactory @inject ITagService tagService @inject IMediaService mediaService +@attribute [Authorize] @title diff --git a/Program.cs b/Program.cs index 564ab30..e78f0d4 100644 --- a/Program.cs +++ b/Program.cs @@ -1,12 +1,16 @@ using Microsoft.EntityFrameworkCore; using System.Text.Json.Serialization; using HyperBooru.Services; +using Microsoft.AspNetCore.Authentication.Cookies; namespace HyperBooru; public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); + builder.Services.AddHttpContextAccessor(); + builder.Services.AddAuthentication( + CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(); builder.Services.AddControllers().AddJsonOptions(o => { var converter = new JsonStringEnumConverter(); o.JsonSerializerOptions.Converters.Add(converter); @@ -20,7 +24,7 @@ public class Program { builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); - builder.Services.AddSingleton(); + builder.Services.AddScoped(); builder.Services.AddHostedService(); var app = builder.Build(); diff --git a/Services/UserService.cs b/Services/UserService.cs index d2abea3..1a2bd8f 100644 --- a/Services/UserService.cs +++ b/Services/UserService.cs @@ -1,9 +1,14 @@ -namespace HyperBooru.Services; +using Microsoft.AspNetCore.Cryptography.KeyDerivation; +using Microsoft.EntityFrameworkCore; + +namespace HyperBooru.Services; public interface IUserService { public bool ShowNsfw { get; set; } public event EventHandler ShowNsfwChanged; + + public User User { get; } } public class UserService : IUserService { @@ -15,7 +20,42 @@ public class UserService : IUserService { } } + public User User { + get { + if(user is not null) + return user; + using var db = dbFactory.CreateDbContext(); + int id = int.Parse(httpContextAccessor + .HttpContext!.User.Claims + .First(c => c.Type == "ObjectId") + .Value); + return user = db.Users.Find(id)!; + } + } + public event EventHandler ShowNsfwChanged; private bool showNsfw = false; + + private User? user; + + private IDbContextFactory dbFactory; + private IHttpContextAccessor httpContextAccessor; + + public UserService( + IDbContextFactory dbFactory, + IHttpContextAccessor httpContextAccessor) { + + this.dbFactory = dbFactory; + this.httpContextAccessor = httpContextAccessor; + } + + public static string HashPassword(string password) => + Convert.ToBase64String( + KeyDerivation.Pbkdf2( + password, + Array.Empty(), + KeyDerivationPrf.HMACSHA512, + 100_000, + 512 / 8)); } diff --git a/User.cs b/User.cs new file mode 100644 index 0000000..61ef03f --- /dev/null +++ b/User.cs @@ -0,0 +1,9 @@ +using Microsoft.EntityFrameworkCore; + +namespace HyperBooru; + +[Index(nameof(Username))] +public class User : HBObject { + public string Username { get; set; } + public string PasswordHash { get; set; } +} diff --git a/_Imports.razor b/_Imports.razor index d20a491..4c5566b 100644 --- a/_Imports.razor +++ b/_Imports.razor @@ -1,6 +1,8 @@ @using HyperBooru @using HyperBooru.Pages.Component @using HyperBooru.Services +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @using Microsoft.EntityFrameworkCore diff --git a/wwwroot/images/loginbg.webp b/wwwroot/images/loginbg.webp new file mode 100644 index 0000000..759e666 Binary files /dev/null and b/wwwroot/images/loginbg.webp differ diff --git a/wwwroot/js/keyboard.js b/wwwroot/js/keyboard.js index e3854f9..392a7d1 100644 --- a/wwwroot/js/keyboard.js +++ b/wwwroot/js/keyboard.js @@ -1,7 +1,7 @@ async function keyDownHandler(e) { function isDialogChild(e) { - while (e = e.parentElement) - if (e.tagName == 'DIV' && e.classList.contains('dialog')) + while(e = e.parentElement) + if(e.tagName == 'DIV' && e.classList.contains('dialog')) return true; return false; } diff --git a/wwwroot/loginbg.webm b/wwwroot/loginbg.webm new file mode 100644 index 0000000..139ed0d Binary files /dev/null and b/wwwroot/loginbg.webm differ -- cgit v1.3