summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--App.razor30
-rw-r--r--Controllers/LoginController.cs50
-rw-r--r--HBContext.cs25
-rw-r--r--MainLayout.razor22
-rw-r--r--MainLayout.razor.css53
-rw-r--r--Media.cs2
-rw-r--r--Pages/Component/RedirectLogin.razor6
-rw-r--r--Pages/Component/Titlebar.razor77
-rw-r--r--Pages/Component/Titlebar.razor.css79
-rw-r--r--Pages/Gallery.razor1
-rw-r--r--Pages/Login.razor17
-rw-r--r--Pages/Login.razor.css6
-rw-r--r--Pages/TagDefinitions.razor1
-rw-r--r--Pages/Upload.razor1
-rw-r--r--Pages/ViewMedia.razor1
-rw-r--r--Program.cs6
-rw-r--r--Services/UserService.cs42
-rw-r--r--User.cs9
-rw-r--r--_Imports.razor2
-rw-r--r--wwwroot/images/loginbg.webpbin0 -> 2247672 bytes
-rw-r--r--wwwroot/js/keyboard.js4
-rw-r--r--wwwroot/loginbg.webmbin0 -> 390877 bytes
22 files changed, 340 insertions, 94 deletions
diff --git a/App.razor b/App.razor
index d414dad..b4e47c9 100644
--- a/App.razor
+++ b/App.razor
@@ -1,11 +1,19 @@
-<Router AppAssembly="@typeof(App).Assembly">
- <Found Context="routeData">
- <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
- </Found>
- <NotFound>
- <PageTitle>Not found</PageTitle>
- <LayoutView Layout="@typeof(MainLayout)">
- <p role="alert">Sorry, there's nothing at this address.</p>
- </LayoutView>
- </NotFound>
-</Router>
+<CascadingAuthenticationState>
+ <Router AppAssembly="@typeof(App).Assembly">
+ <Found Context="routeData">
+ <AuthorizeRouteView
+ RouteData="@routeData"
+ DefaultLayout="@typeof(MainLayout)">
+ <NotAuthorized>
+ <RedirectLogin/>
+ </NotAuthorized>
+ </AuthorizeRouteView>
+ </Found>
+ <NotFound>
+ <PageTitle>Not found</PageTitle>
+ <LayoutView Layout="@typeof(MainLayout)">
+ <p role="alert">Sorry, there's nothing at this address.</p>
+ </LayoutView>
+ </NotFound>
+ </Router>
+</CascadingAuthenticationState>
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<IActionResult> 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<HBObject> Objects { get; set; }
+ public DbSet<User> Users { get; set; }
public DbSet<TagDefinition> TagDefinitions { get; set; }
public DbSet<Tag> Tags { get; set; }
public DbSet<Media> Media { get; set; }
@@ -42,19 +47,29 @@ public class HBContext : DbContext {
// These should NEVER change
modelBuilder.Entity<TagDefinition>().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<User>().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<TagDefinition>()
diff --git a/MainLayout.razor b/MainLayout.razor
index 2d3cadc..5d68b65 100644
--- a/MainLayout.razor
+++ b/MainLayout.razor
@@ -2,28 +2,8 @@
<link href="@(nameof(HyperBooru)).styles.css" rel="stylesheet" />
-<div id="navbar">
- <a href="/">Home</a>
- <a href="/TagDefinitions">Tags</a>
- <a href="/Gallery?ingest=true">Ingest</a>
- <a href="/Upload">Upload</a>
- <a href="javascript:;" @onclick=@(() => aboutDialog.Show())>About</a>
-
- <p id="nsfw-label">NSFW</p>
- <div id="nsfw-switch">
- <NsfwSwitch/>
- </div>
- <form action="/Gallery" method="get">
- <input type="text" name="q" placeholder="Search"/>
- </form>
-</div>
+<Titlebar/>
<div id="content">
@Body
</div>
-
-<AboutDialog @ref=aboutDialog/>
-
-@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
+
+<script suppress-error="BL9992">
+ async function login() {
+ var username = document.querySelector('input#username');
+ var password = document.querySelector('input#password');
+
+ var formData = new FormData();
+ formData.append('username', username.value);
+ formData.append('password', password.value);
+
+ var resp = await fetch('/Login', {
+ method: 'POST',
+ body: formData
+ });
+
+ if(resp.ok) {
+ window.location.href = '/';
+ } else if(resp.status == 403) {
+ var form = document.querySelector('form.login');
+ form.classList.remove('bad-login');
+ @* TODO: improve this hacky method of triggering reflow *@
+ form.offsetWidth;
+ form.classList.add('bad-login');
+ username.value = password.value = null;
+ username.focus();
+ } else {
+ alert('Unknown error while attempting to login!');
+ }
+ }
+
+ async function logout() {
+ var resp = await fetch('/Logout', { method: 'POST' });
+ if(resp.ok) {
+ window.location.href = '/Login';
+ } else {
+ alert('Error logging out!');
+ }
+ }
+</script>
+
+<AuthorizeView>
+ <Authorized>
+ <div id="navbar">
+ <a href="/">Home</a>
+ <a href="/TagDefinitions">Tags</a>
+ <a href="/Gallery?ingest=true">Ingest</a>
+ <a href="/Upload">Upload</a>
+ <a href="javascript:;" @onclick=@(() => aboutDialog.Show())>About</a>
+
+ <p id="nsfw-label">NSFW</p>
+ <div id="nsfw-switch">
+ <NsfwSwitch/>
+ </div>
+ <form action="/Gallery" method="get">
+ <input type="text" name="q" placeholder="Search"/>
+ </form>
+ <a href="javascript:logout();">Logout</a>
+ </div>
+ <AboutDialog @ref=aboutDialog/>
+ </Authorized>
+ <NotAuthorized>
+ <div id="navbar">
+ <h2>Login</h2>
+ <form class="login" action="javascript:login();">
+ <input id="username" placeholder="Username" type="text"/>
+ <input id="password" placeholder="Password" type="password"/>
+ </form>
+ <a href="javascript:login();">Login</a>
+ </div>
+ </NotAuthorized>
+</AuthorizeView>
+
+@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]
<PageTitle>@Title</PageTitle>
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
+
+<PageTitle>HyperBooru Login</PageTitle>
+
+<div/>
+
+@code {
+ [CascadingParameter]
+ public Task<AuthenticationState> 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]
<PageTitle>Tag Definitions</PageTitle>
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]
<div id="dropzone">
<p>Drag a file to upload it<br/>or click to select one or more file(s)</p>
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<HBContext> dbFactory
@inject ITagService tagService
@inject IMediaService mediaService
+@attribute [Authorize]
<PageTitle>@title</PageTitle>
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<ISearchService, SearchService>();
builder.Services.AddScoped<ITagService, TagService>();
builder.Services.AddScoped<IMediaService, MediaService>();
- builder.Services.AddSingleton<IUserService, UserService>();
+ builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddHostedService<OcrService>();
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<bool> 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<bool> ShowNsfwChanged;
private bool showNsfw = false;
+
+ private User? user;
+
+ private IDbContextFactory<HBContext> dbFactory;
+ private IHttpContextAccessor httpContextAccessor;
+
+ public UserService(
+ IDbContextFactory<HBContext> dbFactory,
+ IHttpContextAccessor httpContextAccessor) {
+
+ this.dbFactory = dbFactory;
+ this.httpContextAccessor = httpContextAccessor;
+ }
+
+ public static string HashPassword(string password) =>
+ Convert.ToBase64String(
+ KeyDerivation.Pbkdf2(
+ password,
+ Array.Empty<byte>(),
+ 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
--- /dev/null
+++ b/wwwroot/images/loginbg.webp
Binary files 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
--- /dev/null
+++ b/wwwroot/loginbg.webm
Binary files differ