summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJake Mannens <jake@asger.xyz>2023-10-16 02:01:27 +1100
committerJake Mannens <jake@asger.xyz>2023-10-16 02:01:27 +1100
commit07728d1048f34e1d048da63684b341ab30bc1d06 (patch)
tree9c92b30be5f3cd060827edc2ff5836b6122a9cdd
parentea89ec0c1b05ac246f2ffd5907daace27564100b (diff)
FeedService and AclDialog
-rw-r--r--.config/dotnet-tools.json2
-rw-r--r--Controllers/LoginController.cs1
-rw-r--r--LocalPrincipal.cs1
-rw-r--r--Pages/Component/AclDialog.razor93
-rw-r--r--Pages/Component/MiniPrincipalSelect.razor59
-rw-r--r--Pages/Component/MiniPrincipalSelect.razor.css19
-rw-r--r--Pages/Component/Titlebar.razor5
-rw-r--r--Pages/ViewMedia.razor1
-rw-r--r--Pages/_Host.cshtml1
-rw-r--r--PrincipalProviders/LocalPrincipalProvider.cs22
-rw-r--r--SecurityIdentifier.cs2
-rw-r--r--Server.csproj8
-rw-r--r--Services/FeedService.cs78
-rw-r--r--Services/PrincipalProvider.cs7
-rw-r--r--Services/SecurityService.cs10
-rw-r--r--wwwroot/js/shake.js9
16 files changed, 253 insertions, 65 deletions
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 558293e..96be437 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"dotnet-ef": {
- "version": "7.0.10",
+ "version": "7.0.11",
"commands": [
"dotnet-ef"
]
diff --git a/Controllers/LoginController.cs b/Controllers/LoginController.cs
index ebadf37..55842d9 100644
--- a/Controllers/LoginController.cs
+++ b/Controllers/LoginController.cs
@@ -1,7 +1,6 @@
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;
diff --git a/LocalPrincipal.cs b/LocalPrincipal.cs
index f4b9981..6a12bad 100644
--- a/LocalPrincipal.cs
+++ b/LocalPrincipal.cs
@@ -18,6 +18,7 @@ public class LocalPrincipal : IPrincipal {
// displaying the user's name consistently, but may lead to
// unexpected behaviour in future when setting the DisplayName
// property if that ever becomes possible.
+ [NotMapped]
public string DisplayName {
get => Name;
set => Name = value;
diff --git a/Pages/Component/AclDialog.razor b/Pages/Component/AclDialog.razor
index 691e984..c924b98 100644
--- a/Pages/Component/AclDialog.razor
+++ b/Pages/Component/AclDialog.razor
@@ -1,20 +1,24 @@
@using System.Numerics;
@inject HBContext db
+@inject ISecurityService securityService
@implements IDialog
<Dialog HeightPixels=500 WidthPixels=900 Title="Edit permissions" @ref=dialog>
<div class="vcontainer">
<div class="hcontainer">
<div>
- @if(obj?.Acl is not null) {
- <MiniPrincipalSelect Label="Owner"/>
- <table class="data-table">
- <tr>
- <th>Action</th>
- <th>Subject</th>
- <th colspan="2">Permissions</th>
- </tr>
- @foreach(var rule in obj.Acl.Rules.OrderByDescending(r => r.Action)) {
+ <MiniPrincipalSelect
+ Label="Owner"
+ OnChange=@((sid) => obj.Owner = sid)
+ @ref=ownerSelect/>
+ <table class="data-table">
+ <tr>
+ <th>Action</th>
+ <th>Subject</th>
+ <th colspan="2">Permissions</th>
+ </tr>
+ @if(obj?.Acl is not null) {
+ @foreach(var rule in obj.Acl.Rules) {
<tr>
<td>
<div>
@@ -24,24 +28,37 @@
</div>
</td>
<td>
- @rule.Principal.ToString()
+ @if(rule.Principal != WellKnownSid.NullSid) {
+ @(securityService.TranslateName(rule.Principal))
+ } else {
+ <i style="color:var(--col-error-pri);">Select a user/group!</i>
+ }
+ </td>
+ <td>
+ @if(rule.Permissions == 0) {
+ <i>None</i>
+ } else {
+ @GetActivePermissions(rule);
+ }
</td>
- <td>@GetActivePermissions(rule)</td>
<td>
<a title="Edit" href="javascript:;" @onclick=@(() => EditRule(rule))>&#x1F589</a>
<a title="Delete" href="javascript:;" @onclick=@(() => RemoveRule(rule))>&#x2716</a>
</td>
</tr>
}
- </table>
- <br/>
- <center><a href="javascript:;" @onclick=AddRule>Add new</a></center>
- }
+ }
+ </table>
+ <br/>
+ <center><a href="javascript:;" @onclick=AddRule>Add new</a></center>
</div>
<div>
@if(ruleToEdit is not null && permissionCheckboxes is not null) {
- <MiniPrincipalSelect Label="Subject"/>
- var permissions = Acl.GetPermissionDescriptions(obj)
+ <MiniPrincipalSelect
+ Label="Subject"
+ OnChange=@((sid) => ruleToEdit.Principal = sid)
+ @ref=subjectSelect/>
+ var permissions = Acl.GetPermissionDescriptions(obj!)
.OrderByDescending(kv => BitOperations.PopCount(kv.Value))
.ThenBy(kv => kv.Value);
foreach(var perm in permissionCheckboxes) {
@@ -60,7 +77,7 @@
<ButtonContainer>
<button class="secondary" @onclick=Hide>Cancel</button>
@if(obj?.Acl is not null) {
- <button data-keyboard-shortcut="a" @onclick=ApplyAcl disabled=@(ApplyDisabled)>
+ <button data-keyboard-shortcut="a" @onclick=ApplyAcl disabled=@ApplyDisabled>
<u>A</u>pply
</button>
}
@@ -91,7 +108,9 @@
private string? editOwner;
private string? editSubject;
- private Dialog dialog;
+ private Dialog dialog;
+ private MiniPrincipalSelect ownerSelect;
+ private MiniPrincipalSelect? subjectSelect;
public void Show() => Visible = true;
public void Hide() => Visible = false;
@@ -107,6 +126,8 @@
editOwner = null;
CancelEditRule();
+ db.ChangeTracker.Clear();
+
obj = db.Objects
.Include(o => o.Acl)
.First(o => o.ObjectId == value.ObjectId);
@@ -121,22 +142,29 @@
addedAcl = true;
}
+ ownerSelect.SecurityIdentifier = obj.Owner;
lastHashCode = GetAclHashCode(obj.Acl);
}
}
public bool ApplyDisabled =>
- #if DEBUG
- false;
- #else
GetAclHashCode(obj.Acl!) == lastHashCode ||
obj.Acl!.Rules.Select(r => r.Principal).Contains(WellKnownSid.NullSid);
- #endif
+
+ protected override void OnAfterRender(bool firstRender) {
+ if(subjectSelect is null || ruleToEdit is null)
+ return;
+ if(subjectSelect.SecurityIdentifier is not null)
+ return;
+
+ subjectSelect.SecurityIdentifier = ruleToEdit.Principal;
+ StateHasChanged();
+ }
private string GetActivePermissions(AclRule rule) {
var perms = Acl.GetPermissionDescriptions(obj)
- .Where(kv => (rule.Permissions & kv.Value) == kv.Value)
- .ToList();
+ .Where(kv => (rule.Permissions & kv.Value) == kv.Value)
+ .ToList();
// Filter the list of matching permissions to include the
// most relevant encapsulation permissions only. E.g. if
@@ -159,6 +187,12 @@
}
private void ApplyAcl() {
+ if(obj.Acl!.Rules.Count() == 0) {
+ obj.Acl = null;
+ if(!addedAcl)
+ db.Remove(obj.Acl!);
+ }
+
db.SaveChanges();
Hide();
}
@@ -198,14 +232,17 @@
// Special hash function to identify only the elements of
// the ACL that may have been changed by the user via this
// dialog.
- private int GetAclHashCode(Acl acl) =>
- !acl.Rules.Any() ? 0 : acl.Rules
+ private int GetAclHashCode(Acl acl) {
+ var aclHash = !acl.Rules.Any() ? 0 : acl.Rules
.Select(r => (
r.Action,
r.Permissions,
r.Principal.GetHashCode()).GetHashCode())
.Aggregate((a, v) => HashCode.Combine(a, v));
+ return HashCode.Combine(aclHash, obj.Owner.GetHashCode());
+ }
+
private class PermissionCheckbox {
public string Description { get; private init; }
@@ -228,4 +265,4 @@
}
}
}
-} \ No newline at end of file
+}
diff --git a/Pages/Component/MiniPrincipalSelect.razor b/Pages/Component/MiniPrincipalSelect.razor
index 2202b95..89409bd 100644
--- a/Pages/Component/MiniPrincipalSelect.razor
+++ b/Pages/Component/MiniPrincipalSelect.razor
@@ -1,18 +1,19 @@
@inject ISecurityService securityService
+@inject IJSRuntime jsRuntime
-<div>
- @if(edit) {
+<div @ref=div>
+ @if(newName is not null) {
<label>@Label</label>
- <input type="text" autocomplete="off" @bind=name/>
- <button class="secondary" @onclick=@(() => Edit(false))>Cancel</button>
+ <input type="text" autocomplete="off" @bind=newName/>
+ <button class="secondary" @onclick=Cancel>Cancel</button>
<button @onclick=Submit>OK</button>
} else {
<label>@(Label):</label>
- <a href="javascript:;" @onclick=@(() => Edit(true))>
+ <a href="javascript:;" @onclick=Edit>
@if(SecurityIdentifier is null || SecurityIdentifier == WellKnownSid.NullSid) {
<i>Please select a user or group</i>
} else {
- @securityService.TranslateName(SecurityIdentifier)
+ @name
}
</a>
}
@@ -25,20 +26,44 @@
[Parameter]
public EventCallback<SecurityIdentifier> OnChange { get; set; }
- private bool edit = false;
- private string name;
+ private bool edit = false;
+ private string? name;
+ private string? newName;
- public SecurityIdentifier? SecurityIdentifier { get; set; }
+ private ElementReference div;
- private void Edit(bool enableEdit) {
- edit = enableEdit;
+ private HyperBooru.SecurityIdentifier? securityIdentifier;
- if(enableEdit)
- name = SecurityIdentifier is null ? "" :
- securityService.TranslateName(SecurityIdentifier);
+ public SecurityIdentifier? SecurityIdentifier {
+ get => securityIdentifier;
+ set {
+ securityIdentifier = value;
+ name = value is null ? null : securityService.TranslateName(value);
+ }
}
- private void Submit() {
- Edit(false);
+ private void Edit() {
+ newName = SecurityIdentifier == WellKnownSid.NullSid ? "" : name;
+ StateHasChanged();
}
-} \ No newline at end of file
+
+ private async void Cancel() {
+ newName = null;
+ await jsRuntime.InvokeVoidAsync("removeClass", div, "bad-principal");
+ StateHasChanged();
+ }
+
+ private async void Submit() {
+ var sid = securityService.TranslateName(newName!);
+ if(sid is null) {
+ await jsRuntime.InvokeVoidAsync("cycleClass", div, "bad-principal");
+ return;
+ }
+
+ await jsRuntime.InvokeVoidAsync("removeClass", div, "bad-principal");
+ SecurityIdentifier = sid;
+ await OnChange.InvokeAsync(sid);
+ newName = null;
+ StateHasChanged();
+ }
+}
diff --git a/Pages/Component/MiniPrincipalSelect.razor.css b/Pages/Component/MiniPrincipalSelect.razor.css
index 4b7a217..7e410fc 100644
--- a/Pages/Component/MiniPrincipalSelect.razor.css
+++ b/Pages/Component/MiniPrincipalSelect.razor.css
@@ -5,14 +5,27 @@
margin-bottom: 16px;
}
-div * {
+div > :not(div) {
margin: 0;
}
-div :not(:last-child) {
+div > :not(:last-child) {
margin-right: 5px;
}
-div input[type="text"] {
+div > input[type="text"] {
flex-grow: 1;
}
+
+div.bad-principal > :not(div) {
+ animation-iteration-count: 3;
+ animation-timing-function: linear;
+ animation: bad-principal 0.2s;
+}
+
+@keyframes bad-principal {
+ 0% { transform: translateX(0); }
+ 33% { transform: translateX(-15px); }
+ 66% { transform: translateX(+15px); }
+ 100% { transform: translateX(0); }
+}
diff --git a/Pages/Component/Titlebar.razor b/Pages/Component/Titlebar.razor
index 766787a..73700bc 100644
--- a/Pages/Component/Titlebar.razor
+++ b/Pages/Component/Titlebar.razor
@@ -15,10 +15,7 @@
if(resp.ok) {
window.location.href = '/';
} else if(resp.status == 403) {
- form.classList.remove('bad-login');
- @* TODO: improve this hacky method of triggering reflow *@
- form.offsetWidth;
- form.classList.add('bad-login');
+ cycleClass(form, 'bad-login');
inputs.forEach(e => e.value = null);
inputs[0].focus();
} else {
diff --git a/Pages/ViewMedia.razor b/Pages/ViewMedia.razor
index f6a1382..05cf700 100644
--- a/Pages/ViewMedia.razor
+++ b/Pages/ViewMedia.razor
@@ -83,7 +83,6 @@
</div>
<div id="button-container">
<ButtonContainer>
- <button @onclick=@(() => { aclDialog.Object = media; aclDialog.Show(); }) class="secondary" data-keyboard-shortcut="p">Edit <u>P</u>ermissions</button>
<button @onclick=@(() => deleteDialog.Show()) class="warning" data-keyboard-shortcut="d"><u>D</u>elete</button>
<button @onclick=@(() => aclDialog.Show(media)) class="secondary" data-keyboard-shortcut="p">Edit <u>P</u>ermissions</button>
<button @onclick=@(() => tagDialog.Show()) class="secondary" data-keyboard-shortcut="t">Add <u>T</u>ag</button>
diff --git a/Pages/_Host.cshtml b/Pages/_Host.cshtml
index 69bced8..20901d2 100644
--- a/Pages/_Host.cshtml
+++ b/Pages/_Host.cshtml
@@ -13,6 +13,7 @@
<link href="/favicon.ico" rel="icon" />
<link href="/manifest.webmanifest" rel="manifest" />
<script type="text/javascript" src="/js/keyboard.js"></script>
+ <script type="text/javascript" src="/js/shake.js"></script>
<script type="text/javascript" src="/js/dialog.js"></script>
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
</head>
diff --git a/PrincipalProviders/LocalPrincipalProvider.cs b/PrincipalProviders/LocalPrincipalProvider.cs
index 5c27518..723ff81 100644
--- a/PrincipalProviders/LocalPrincipalProvider.cs
+++ b/PrincipalProviders/LocalPrincipalProvider.cs
@@ -66,6 +66,28 @@ public class LocalPrincipalProvider : PrincipalProvider {
return groups.ToArray();
}
+ public override SecurityIdentifier? TranslateName(string name) {
+ using var db = dbFactory.CreateDbContext();
+ try {
+ return db.Principals
+ .First(p => p.Name.ToLower() == name.ToLower().Trim())
+ .Sid;
+ } catch(InvalidOperationException) {
+ return null;
+ }
+ }
+
+ public override string? TranslateName(SecurityIdentifier sid) {
+ using var db = dbFactory.CreateDbContext();
+ try {
+ return db.Principals
+ .First(p => p.Sid == sid)
+ .Name;
+ } catch(InvalidOperationException) {
+ return null;
+ }
+ }
+
public override bool ValidatePassword(IUser user, string password) =>
((LocalUser) user).PasswordHash == HashPassword(password);
diff --git a/SecurityIdentifier.cs b/SecurityIdentifier.cs
index 98818e0..608cb75 100644
--- a/SecurityIdentifier.cs
+++ b/SecurityIdentifier.cs
@@ -17,7 +17,7 @@ public static class WellKnownSid {
private static readonly (string name, SecurityIdentifier sid)[] nameMap = new[] {
( "Everyone", WorldSid ),
( "LOCAL", LocalSid ),
- ( "CREATOR OWNER", CreatorGroupSid ),
+ ( "CREATOR OWNER", CreatorOwnerSid ),
( "CREATOR GROUP", CreatorGroupSid )
};
diff --git a/Server.csproj b/Server.csproj
index ae08913..f83ab22 100644
--- a/Server.csproj
+++ b/Server.csproj
@@ -21,16 +21,12 @@
<ItemGroup>
<PackageReference Include="Magick.NET-Q16-AnyCPU" Version="13.2.0" />
- <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.11" />
- <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.11">
- <PrivateAssets>all</PrivateAssets>
+ <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.12">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ <PrivateAssets>all</PrivateAssets>
</PackageReference>
- <PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="7.0.11" />
<PackageReference Include="Mime-Detective" Version="23.9.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.11" />
- <PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
- <PackageReference Include="System.Drawing.Common" Version="7.0.0" />
<PackageReference Include="Tesseract" Version="5.2.0" />
</ItemGroup>
diff --git a/Services/FeedService.cs b/Services/FeedService.cs
new file mode 100644
index 0000000..c66b9ee
--- /dev/null
+++ b/Services/FeedService.cs
@@ -0,0 +1,78 @@
+using Microsoft.EntityFrameworkCore;
+using System.Data.Common;
+
+namespace HyperBooru.Services;
+
+public enum FeedOrder {
+ Chronological,
+ Rating
+}
+
+public interface IFeedService {
+ public IEnumerable<Media> Feed { get; }
+
+ public void InitializeFeed(
+ FeedOrder order = FeedOrder.Chronological,
+ bool descending = true,
+ bool randomPosition = false);
+}
+
+public class FeedService : IFeedService {
+ private FeedConfiguration? feedConfig;
+
+ private IDbContextFactory<HBContext> dbFactory;
+
+ public FeedService(IDbContextFactory<HBContext> dbFactory) =>
+ this.dbFactory = dbFactory;
+
+ public void InitializeFeed(
+ FeedOrder order,
+ bool descending,
+ bool randomPosition) {
+
+ feedConfig = new() {
+ Order = order,
+ Descending = descending,
+ RandomPosition = randomPosition
+ };
+ }
+
+ public IEnumerable<Media> Feed {
+ get {
+ if(feedConfig is null)
+ throw new InvalidOperationException("Feed must be initialized first");
+
+ while(true) {
+ var db = dbFactory.CreateDbContext();
+
+ IOrderedQueryable<Media> media;
+
+ switch(feedConfig.Order) {
+ default:
+ case FeedOrder.Chronological:
+ if(feedConfig.Descending)
+ media = db.Media.OrderByDescending(m => m.ObjectId);
+ else
+ media = db.Media.OrderBy(m => m.ObjectId);
+ break;
+ }
+
+ Media[] mediaArray = media.Take(50).ToArray();
+
+ db.Dispose();
+
+ if(mediaArray.Count() == 0)
+ break;
+
+ foreach(var m in mediaArray)
+ yield return m;
+ }
+ }
+ }
+
+ private record FeedConfiguration {
+ public FeedOrder Order { get; set; }
+ public bool Descending { get; set; }
+ public bool RandomPosition { get; set; }
+ }
+}
diff --git a/Services/PrincipalProvider.cs b/Services/PrincipalProvider.cs
index 4b2cf42..7039379 100644
--- a/Services/PrincipalProvider.cs
+++ b/Services/PrincipalProvider.cs
@@ -5,6 +5,9 @@ public interface IPrincipalProvider {
public IUser? GetUser(string name);
public IGroup? GetGroup(string name);
+ public SecurityIdentifier? TranslateName(string name);
+ public string? TranslateName(SecurityIdentifier sid);
+
/// <summary>
/// Perform a search for any principals whose account name
/// matches the search term specified by <c>name</c>.
@@ -29,13 +32,15 @@ public abstract class PrincipalProvider : IPrincipalProvider {
public abstract IUser? GetUser(string name);
public abstract IGroup? GetGroup(string name);
+ public abstract SecurityIdentifier? TranslateName(string name);
+ public abstract string? TranslateName(SecurityIdentifier sid);
+
public abstract IPrincipal[]? SearchPrincipals(string name);
public IGroup[] GetGroups(IPrincipal principal) =>
GetGroups(principal.Sid, false);
public IGroup[] GetGroups(IPrincipal principal, bool recurse) =>
GetGroups(principal.Sid, recurse);
-
public IGroup[] GetGroups(SecurityIdentifier sid) => GetGroups(sid, false);
public abstract IGroup[] GetGroups(SecurityIdentifier sid, bool recurse);
diff --git a/Services/SecurityService.cs b/Services/SecurityService.cs
index 2d23a58..85513ec 100644
--- a/Services/SecurityService.cs
+++ b/Services/SecurityService.cs
@@ -80,15 +80,21 @@ public class SecurityService : ISecurityService {
if(wellKnownSid is not null)
return wellKnownSid;
- return sid.SddlForm;
+ return principalProvider.TranslateName(sid) ?? sid.SddlForm;
}
public SecurityIdentifier? TranslateName(string name) {
+ name = name.Trim().ToLower();
+
+ try {
+ return new SecurityIdentifier(name.ToUpper());
+ } catch(ArgumentException) {}
+
var wellKnownSid = WellKnownSid.TranslateName(name);
if(wellKnownSid is not null)
return wellKnownSid;
- throw new NotImplementedException();
+ return principalProvider.TranslateName(name);
}
public IEnumerable<HBObject> Filter(
diff --git a/wwwroot/js/shake.js b/wwwroot/js/shake.js
new file mode 100644
index 0000000..be984ed
--- /dev/null
+++ b/wwwroot/js/shake.js
@@ -0,0 +1,9 @@
+function cycleClass(element, className) {
+ element.classList.remove(className);
+ element.offsetWidth;
+ element.classList.add(className);
+}
+
+function removeClass(element, className) {
+ element.classList.remove(className);
+}