Reorganized folder structure
This commit is contained in:
16
src/HopFrame.Web/AdminPermissions.cs
Normal file
16
src/HopFrame.Web/AdminPermissions.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace HopFrame.Web;
|
||||
|
||||
[Obsolete("Use HopFrame.Security.AdminPermissions instead")]
|
||||
public static class AdminPermissions {
|
||||
public const string IsAdmin = Security.AdminPermissions.IsAdmin;
|
||||
|
||||
public const string ViewUsers = Security.AdminPermissions.ViewUsers;
|
||||
public const string EditUser = Security.AdminPermissions.EditUser;
|
||||
public const string DeleteUser = Security.AdminPermissions.DeleteUser;
|
||||
public const string AddUser = Security.AdminPermissions.AddUser;
|
||||
|
||||
public const string ViewGroups = Security.AdminPermissions.ViewGroups;
|
||||
public const string EditGroup = Security.AdminPermissions.EditGroup;
|
||||
public const string DeleteGroup = Security.AdminPermissions.DeleteGroup;
|
||||
public const string AddGroup = Security.AdminPermissions.AddGroup;
|
||||
}
|
||||
35
src/HopFrame.Web/AuthMiddleware.cs
Normal file
35
src/HopFrame.Web/AuthMiddleware.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System.Security.Claims;
|
||||
using HopFrame.Database;
|
||||
using HopFrame.Security.Authentication;
|
||||
using HopFrame.Security.Claims;
|
||||
using HopFrame.Security.Services;
|
||||
using HopFrame.Web.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace HopFrame.Web;
|
||||
|
||||
public sealed class AuthMiddleware(IAuthService auth, IPermissionService perms) : IMiddleware {
|
||||
public async Task InvokeAsync(HttpContext context, RequestDelegate next) {
|
||||
var loggedIn = await auth.IsLoggedIn();
|
||||
|
||||
if (!loggedIn) {
|
||||
var token = await auth.RefreshLogin();
|
||||
if (token is null) {
|
||||
await next.Invoke(context);
|
||||
return;
|
||||
}
|
||||
|
||||
var claims = new List<Claim> {
|
||||
new(HopFrameClaimTypes.AccessTokenId, token.Token),
|
||||
new(HopFrameClaimTypes.UserId, token.UserId)
|
||||
};
|
||||
|
||||
var permissions = await perms.GetFullPermissions(token.UserId);
|
||||
claims.AddRange(permissions.Select(perm => new Claim(HopFrameClaimTypes.Permission, perm)));
|
||||
|
||||
context.User.AddIdentity(new ClaimsIdentity(claims, HopFrameAuthentication<HopDbContextBase>.SchemeName));
|
||||
}
|
||||
|
||||
await next?.Invoke(context);
|
||||
}
|
||||
}
|
||||
290
src/HopFrame.Web/Components/Administration/GroupAddModal.razor
Normal file
290
src/HopFrame.Web/Components/Administration/GroupAddModal.razor
Normal file
@@ -0,0 +1,290 @@
|
||||
@rendermode InteractiveServer
|
||||
|
||||
@using BlazorStrap
|
||||
@using BlazorStrap.Shared.Components.Modal
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using BlazorStrap.V5
|
||||
@using CurrieTechnologies.Razor.SweetAlert2
|
||||
@using HopFrame.Database.Models
|
||||
@using HopFrame.Security.Claims
|
||||
@using HopFrame.Security.Services
|
||||
@using HopFrame.Web.Model
|
||||
|
||||
<BSModal DataId="add-group-modal" HideOnValidSubmit="true" IsStaticBackdrop="true" @ref="_modal">
|
||||
<BSForm Model="_group" OnValidSubmit="AddGroup">
|
||||
@if (_isEdit) {
|
||||
<BSModalHeader>Edit group</BSModalHeader>
|
||||
}
|
||||
else {
|
||||
<BSModalHeader>Add group</BSModalHeader>
|
||||
}
|
||||
<BSModalContent>
|
||||
<div class="mb-3">
|
||||
<BSLabel>Name</BSLabel>
|
||||
@if (!_isEdit) {
|
||||
<BSInputGroup>
|
||||
<span class="@BS.Input_Group_Text">group.</span>
|
||||
<BSInput InputType="InputType.Text" @bind-Value="_group.GroupName" required/>
|
||||
</BSInputGroup>
|
||||
}
|
||||
else {
|
||||
<input type="text" class="form-control" disabled value="@_group.Name"/>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (_isEdit) {
|
||||
<div class="mb-3">
|
||||
<BSLabel>Created at</BSLabel>
|
||||
<input type="text" class="form-control" disabled value="@_group.CreatedAt"/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="mb-3">
|
||||
<BSLabel>Description</BSLabel>
|
||||
<BSInput InputType="InputType.TextArea" @bind-Value="_group.Description"/>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<BSInputSwitch @bind-Value="_group.IsDefaultGroup" CheckedValue="true" UnCheckedValue="false">
|
||||
Default group
|
||||
</BSInputSwitch>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<BSLabel>Inherits from</BSLabel>
|
||||
<BSListGroup>
|
||||
<BSListGroupItem>
|
||||
<BSListGroup IsFlush="true">
|
||||
@foreach (var group in _group.Permissions.Where(g => g.PermissionName.StartsWith("group."))) {
|
||||
<BSListGroupItem>
|
||||
<BSButton Color="BSColor.Danger" Size="Size.ExtraSmall" MarginEnd="Margins.Small" OnClick="() => RemovePermission(group)">
|
||||
<HopIconDisplay Type="HopIconDisplay.HopIcon.Cross"/>
|
||||
</BSButton>
|
||||
|
||||
<span>@group.PermissionName.Replace("group.", "")</span>
|
||||
</BSListGroupItem>
|
||||
}
|
||||
</BSListGroup>
|
||||
</BSListGroupItem>
|
||||
<BSListGroupItem>
|
||||
<div style="display: flex; gap: 20px">
|
||||
<BSInput InputType="InputType.Select" @bind-Value="_groupToAdd">
|
||||
<option selected>Select group</option>
|
||||
|
||||
@foreach (var group in _allGroups) {
|
||||
@if (_group.Permissions.All(g => g.PermissionName != group.Name) && group.Name != _group.Name) {
|
||||
<option value="@group.Name">@group.Name.Replace("group.", "")</option>
|
||||
}
|
||||
}
|
||||
</BSInput>
|
||||
<BSButton Color="BSColor.Secondary" OnClick="AddInheritanceGroup">Add</BSButton>
|
||||
</div>
|
||||
</BSListGroupItem>
|
||||
</BSListGroup>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<BSLabel>Permissions</BSLabel>
|
||||
<BSListGroup>
|
||||
<BSListGroupItem>
|
||||
<BSListGroup IsFlush="true">
|
||||
@foreach (var perm in _group.Permissions.Where(perm => !perm.PermissionName.StartsWith("group."))) {
|
||||
<BSListGroupItem>
|
||||
<BSButton Color="BSColor.Danger" Size="Size.ExtraSmall" MarginEnd="Margins.Small" OnClick="() => RemovePermission(perm)">
|
||||
<HopIconDisplay Type="HopIconDisplay.HopIcon.Cross"/>
|
||||
</BSButton>
|
||||
|
||||
<span>@perm.PermissionName</span>
|
||||
</BSListGroupItem>
|
||||
}
|
||||
</BSListGroup>
|
||||
</BSListGroupItem>
|
||||
<BSListGroupItem>
|
||||
<div style="display: flex; gap: 20px">
|
||||
<BSInput InputType="InputType.Text" @bind-Value="_permissionToAdd"/>
|
||||
<BSButton Color="BSColor.Secondary" OnClick="AddPermission">Add</BSButton>
|
||||
</div>
|
||||
</BSListGroupItem>
|
||||
</BSListGroup>
|
||||
</div>
|
||||
</BSModalContent>
|
||||
<BSModalFooter>
|
||||
<BSButton Target="add-group-modal">Cancel</BSButton>
|
||||
<BSButton IsSubmit="true" Color="BSColor.Primary">Save</BSButton>
|
||||
</BSModalFooter>
|
||||
</BSForm>
|
||||
</BSModal>
|
||||
|
||||
@inject IPermissionService Permissions
|
||||
@inject SweetAlertService Alerts
|
||||
@inject ITokenContext Context
|
||||
|
||||
@code {
|
||||
[Parameter] public Func<Task> ReloadPage { get; set; }
|
||||
|
||||
private PermissionGroupAdd _group;
|
||||
|
||||
private BSModalBase _modal;
|
||||
private string _permissionToAdd;
|
||||
private string _groupToAdd;
|
||||
|
||||
private IList<PermissionGroup> _allGroups;
|
||||
|
||||
private bool _isEdit;
|
||||
|
||||
public async Task ShowAsync(PermissionGroup group = null) {
|
||||
_allGroups = await Permissions.GetPermissionGroups();
|
||||
|
||||
if (group is not null) {
|
||||
_group = new PermissionGroupAdd {
|
||||
CreatedAt = group.CreatedAt,
|
||||
Description = group.Description,
|
||||
Name = group.Name,
|
||||
IsDefaultGroup = group.IsDefaultGroup,
|
||||
Permissions = group.Permissions
|
||||
};
|
||||
_isEdit = true;
|
||||
}
|
||||
else {
|
||||
_group = new PermissionGroupAdd {
|
||||
Permissions = new List<Permission>(),
|
||||
IsDefaultGroup = false
|
||||
};
|
||||
_isEdit = false;
|
||||
}
|
||||
|
||||
await _modal.ShowAsync();
|
||||
}
|
||||
|
||||
private async Task AddPermission() {
|
||||
if (string.IsNullOrWhiteSpace(_permissionToAdd)) {
|
||||
await Alerts.FireAsync(new SweetAlertOptions {
|
||||
Title = "Enter a permission name!",
|
||||
Icon = SweetAlertIcon.Error,
|
||||
ShowConfirmButton = true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isEdit) {
|
||||
if (!(await Permissions.HasPermission(Security.AdminPermissions.EditGroup, Context.User.Id))) {
|
||||
await NoEditPermissions();
|
||||
return;
|
||||
}
|
||||
|
||||
await Permissions.AddPermission(_group, _permissionToAdd);
|
||||
}
|
||||
|
||||
_group.Permissions.Add(new Permission {
|
||||
PermissionName = _permissionToAdd
|
||||
});
|
||||
|
||||
_permissionToAdd = null;
|
||||
}
|
||||
|
||||
private async Task RemovePermission(Permission permission) {
|
||||
if (_isEdit) {
|
||||
var perm = await Permissions.GetPermission(permission.PermissionName, _group);
|
||||
await Permissions.RemovePermission(perm);
|
||||
}
|
||||
|
||||
_group.Permissions.Remove(permission);
|
||||
}
|
||||
|
||||
private async Task AddInheritanceGroup() {
|
||||
if (string.IsNullOrWhiteSpace(_groupToAdd)) {
|
||||
await Alerts.FireAsync(new SweetAlertOptions {
|
||||
Title = "Select a group!",
|
||||
Icon = SweetAlertIcon.Error,
|
||||
ShowConfirmButton = true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isEdit) {
|
||||
if (!(await Permissions.HasPermission(Security.AdminPermissions.EditGroup, Context.User.Id))) {
|
||||
await NoEditPermissions();
|
||||
return;
|
||||
}
|
||||
|
||||
await Permissions.AddPermission(_group, _groupToAdd);
|
||||
}
|
||||
|
||||
_group.Permissions.Add(new Permission {
|
||||
PermissionName = _groupToAdd
|
||||
});
|
||||
|
||||
_groupToAdd = null;
|
||||
}
|
||||
|
||||
private async Task AddGroup() {
|
||||
if (_isEdit) {
|
||||
if (!(await Permissions.HasPermission(Security.AdminPermissions.EditGroup, Context.User.Id))) {
|
||||
await NoEditPermissions();
|
||||
return;
|
||||
}
|
||||
|
||||
await Permissions.EditPermissionGroup(_group);
|
||||
|
||||
if (ReloadPage is not null)
|
||||
await ReloadPage.Invoke();
|
||||
|
||||
await Alerts.FireAsync(new SweetAlertOptions {
|
||||
Title = "Group edited!",
|
||||
Icon = SweetAlertIcon.Success,
|
||||
Timer = 1500,
|
||||
ShowConfirmButton = false
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await Permissions.HasPermission(Security.AdminPermissions.AddGroup, Context.User.Id))) {
|
||||
await NoAddPermissions();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_allGroups.Any(group => group.Name == _group.Name)) {
|
||||
await Alerts.FireAsync(new SweetAlertOptions {
|
||||
Title = "Something went wrong!",
|
||||
Text = "This group already exists!",
|
||||
Icon = SweetAlertIcon.Error,
|
||||
ShowConfirmButton = false,
|
||||
Timer = 1500
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var dbGroup = await Permissions.CreatePermissionGroup("group." + _group.GroupName, _group.IsDefaultGroup, _group.Description);
|
||||
|
||||
foreach (var permission in _group.Permissions) {
|
||||
await Permissions.AddPermission(dbGroup, permission.PermissionName);
|
||||
}
|
||||
|
||||
if (ReloadPage is not null)
|
||||
await ReloadPage.Invoke();
|
||||
|
||||
await Alerts.FireAsync(new SweetAlertOptions {
|
||||
Title = "Group added!",
|
||||
Icon = SweetAlertIcon.Success,
|
||||
Timer = 1500,
|
||||
ShowConfirmButton = false
|
||||
});
|
||||
}
|
||||
|
||||
private async Task NoEditPermissions() {
|
||||
await Alerts.FireAsync(new SweetAlertOptions {
|
||||
Title = "Unauthorized!",
|
||||
Text = "You don't have the required permissions to edit a group!",
|
||||
Icon = SweetAlertIcon.Error
|
||||
});
|
||||
}
|
||||
|
||||
private async Task NoAddPermissions() {
|
||||
await Alerts.FireAsync(new SweetAlertOptions {
|
||||
Title = "Unauthorized!",
|
||||
Text = "You don't have the required permissions to add a group!",
|
||||
Icon = SweetAlertIcon.Error
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
@switch (Type) {
|
||||
case HopIcon.Reload:
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="@GetClass()" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2z"/>
|
||||
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466"/>
|
||||
</svg>
|
||||
break;
|
||||
|
||||
case HopIcon.ArrowUp:
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="@GetClass()" viewBox="0 0 16 16">
|
||||
<path d="M7.247 11.14 2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z"/>
|
||||
</svg>
|
||||
break;
|
||||
|
||||
case HopIcon.ArrowDown:
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="@GetClass()" viewBox="0 0 16 16">
|
||||
<path d="m7.247 4.86-4.796 5.481c-.566.647-.106 1.659.753 1.659h9.592a1 1 0 0 0 .753-1.659l-4.796-5.48a1 1 0 0 0-1.506 0z"/>
|
||||
</svg>
|
||||
break;
|
||||
|
||||
case HopIcon.Cross:
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="@GetClass()" viewBox="0 0 16 16">
|
||||
<path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8z"/>
|
||||
</svg>
|
||||
break;
|
||||
|
||||
case HopIcon.User:
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="@GetClass()" viewBox="0 0 16 16">
|
||||
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6"/>
|
||||
</svg>
|
||||
break;
|
||||
|
||||
case HopIcon.Group:
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="@GetClass()" viewBox="0 0 16 16">
|
||||
<path d="M7 14s-1 0-1-1 1-4 5-4 5 3 5 4-1 1-1 1zm4-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6m-5.784 6A2.24 2.24 0 0 1 5 13c0-1.355.68-2.75 1.936-3.72A6.3 6.3 0 0 0 5 9c-4 0-5 3-5 4s1 1 1 1zM4.5 8a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5"/>
|
||||
</svg>
|
||||
break;
|
||||
|
||||
case HopIcon.Logout:
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="@GetClass()" viewBox="0 0 16 16">
|
||||
<path fill-rule="evenodd" d="M6 12.5a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5v-9a.5.5 0 0 0-.5-.5h-8a.5.5 0 0 0-.5.5v2a.5.5 0 0 1-1 0v-2A1.5 1.5 0 0 1 6.5 2h8A1.5 1.5 0 0 1 16 3.5v9a1.5 1.5 0 0 1-1.5 1.5h-8A1.5 1.5 0 0 1 5 12.5v-2a.5.5 0 0 1 1 0z"/>
|
||||
<path fill-rule="evenodd" d="M.146 8.354a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L1.707 7.5H10.5a.5.5 0 0 1 0 1H1.707l2.147 2.146a.5.5 0 0 1-.708.708z"/>
|
||||
</svg>
|
||||
break;
|
||||
}
|
||||
|
||||
<style>
|
||||
svg.bi-nav {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-right: 0.75rem;
|
||||
top: -1px;
|
||||
background-size: cover;
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
[Parameter] public HopIcon Type { get; set; }
|
||||
[Parameter] public bool NavIcon { get; set; }
|
||||
|
||||
public enum HopIcon {
|
||||
Reload,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
User,
|
||||
Group,
|
||||
Logout,
|
||||
Cross
|
||||
}
|
||||
|
||||
private string GetClass() {
|
||||
return NavIcon ? "bi-nav" : "bi";
|
||||
}
|
||||
}
|
||||
131
src/HopFrame.Web/Components/Administration/UserAddModal.razor
Normal file
131
src/HopFrame.Web/Components/Administration/UserAddModal.razor
Normal file
@@ -0,0 +1,131 @@
|
||||
@rendermode InteractiveServer
|
||||
|
||||
@using BlazorStrap
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using BlazorStrap.Shared.Components.Modal
|
||||
@using BlazorStrap.V5
|
||||
@using CurrieTechnologies.Razor.SweetAlert2
|
||||
@using HopFrame.Database.Models
|
||||
@using HopFrame.Security.Claims
|
||||
@using HopFrame.Security.Services
|
||||
@using HopFrame.Web.Model
|
||||
|
||||
<BSModal DataId="add-user-modal" HideOnValidSubmit="true" IsStaticBackdrop="true" OnShow="() => _user = new()" @ref="_modal">
|
||||
<BSForm Model="_user" OnValidSubmit="AddUser">
|
||||
<BSModalHeader>Add user</BSModalHeader>
|
||||
<BSModalContent>
|
||||
<div class="mb-3">
|
||||
<BSLabel>E-Mail</BSLabel>
|
||||
<BSInput InputType="InputType.Email" @bind-Value="_user.Email" required/>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<BSLabel>Username</BSLabel>
|
||||
<BSInput InputType="InputType.Text" @bind-Value="_user.Username" required/>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<BSLabel>Password</BSLabel>
|
||||
<BSInput InputType="InputType.Password" @bind-Value="_user.Password" required/>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<BSLabel>Primary group</BSLabel>
|
||||
<BSInput InputType="InputType.Select" @bind-Value="_user.Group">
|
||||
<option value="">Select group</option>
|
||||
|
||||
@foreach (var group in _allGroups) {
|
||||
<option value="@group.Name">@group.Name.Replace("group.", "")</option>
|
||||
}
|
||||
</BSInput>
|
||||
</div>
|
||||
</BSModalContent>
|
||||
<BSModalFooter>
|
||||
<BSButton Target="add-user-modal">Cancel</BSButton>
|
||||
<BSButton IsSubmit="true" Color="BSColor.Primary">Save</BSButton>
|
||||
</BSModalFooter>
|
||||
</BSForm>
|
||||
</BSModal>
|
||||
|
||||
@inject IUserService Users
|
||||
@inject IPermissionService Permissions
|
||||
@inject SweetAlertService Alerts
|
||||
@inject ITokenContext Auth
|
||||
|
||||
@code {
|
||||
[Parameter] public Func<Task> ReloadPage { get; set; }
|
||||
|
||||
private IList<PermissionGroup> _allGroups = new List<PermissionGroup>();
|
||||
private IList<User> _allUsers = new List<User>();
|
||||
private UserAdd _user;
|
||||
|
||||
private BSModalBase _modal;
|
||||
|
||||
public async Task ShowAsync() {
|
||||
_allGroups = await Permissions.GetPermissionGroups();
|
||||
_allUsers = await Users.GetUsers();
|
||||
|
||||
await _modal.ShowAsync();
|
||||
}
|
||||
|
||||
private async Task AddUser() {
|
||||
if (!(await Permissions.HasPermission(Security.AdminPermissions.AddUser, Auth.User.Id))) {
|
||||
await NoAddPermissions();
|
||||
return;
|
||||
}
|
||||
|
||||
string errorMessage = null;
|
||||
|
||||
if (_allUsers.Any(user => user.Username == _user.Username)) {
|
||||
errorMessage = "Username is already taken!";
|
||||
}
|
||||
else if (_allUsers.Any(user => user.Email == _user.Email)) {
|
||||
errorMessage = "E-Mail is already taken!";
|
||||
}
|
||||
else if (!_user.PasswordIsValid) {
|
||||
errorMessage = "The password needs to be at least 8 characters long!";
|
||||
}
|
||||
else if (!_user.EmailIsValid) {
|
||||
errorMessage = "Invalid E-Mail address!";
|
||||
}
|
||||
else if (string.IsNullOrWhiteSpace(_user.Username)) {
|
||||
errorMessage = "You need to set a username!";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(errorMessage)) {
|
||||
await Alerts.FireAsync(new SweetAlertOptions {
|
||||
Title = "Something went wrong!",
|
||||
Text = errorMessage,
|
||||
Icon = SweetAlertIcon.Error,
|
||||
ShowConfirmButton = false,
|
||||
Timer = 1500
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var user = await Users.AddUser(_user);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_user.Group)) {
|
||||
await Permissions.AddPermission(user, _user.Group);
|
||||
}
|
||||
|
||||
await ReloadPage.Invoke();
|
||||
|
||||
await Alerts.FireAsync(new SweetAlertOptions {
|
||||
Title = "New user added!",
|
||||
Icon = SweetAlertIcon.Success,
|
||||
ShowConfirmButton = false,
|
||||
Timer = 1500
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
private async Task NoAddPermissions() {
|
||||
await Alerts.FireAsync(new SweetAlertOptions {
|
||||
Title = "Unauthorized!",
|
||||
Text = "You don't have the required permissions to add a user!",
|
||||
Icon = SweetAlertIcon.Error
|
||||
});
|
||||
}
|
||||
}
|
||||
306
src/HopFrame.Web/Components/Administration/UserEditModal.razor
Normal file
306
src/HopFrame.Web/Components/Administration/UserEditModal.razor
Normal file
@@ -0,0 +1,306 @@
|
||||
@rendermode InteractiveServer
|
||||
|
||||
@using BlazorStrap
|
||||
@using BlazorStrap.Shared.Components.Modal
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using BlazorStrap.V5
|
||||
@using CurrieTechnologies.Razor.SweetAlert2
|
||||
@using HopFrame.Database.Models
|
||||
@using HopFrame.Security.Claims
|
||||
@using HopFrame.Security.Services
|
||||
@using HopFrame.Web.Model
|
||||
|
||||
<BSModal DataId="edit-user-modal" HideOnValidSubmit="true" IsStaticBackdrop="true" @ref="_modal">
|
||||
<BSForm Model="_user" OnValidSubmit="EditUser">
|
||||
<BSModalHeader>Edit @_user.Username</BSModalHeader>
|
||||
<BSModalContent>
|
||||
<div class="mb-3">
|
||||
<BSLabel>User id</BSLabel>
|
||||
<input type="text" class="form-control" disabled value="@_user.Id"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<BSLabel>Created at</BSLabel>
|
||||
<input type="text" class="form-control" disabled value="@_user.CreatedAt"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<BSLabel>E-Mail</BSLabel>
|
||||
<BSInput InputType="InputType.Email" @bind-Value="_user.Email" required/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<BSLabel>Username</BSLabel>
|
||||
<BSInput InputType="InputType.Text" @bind-Value="_user.Username" required/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<BSLabel>Password</BSLabel>
|
||||
<BSInput InputType="InputType.Password" @bind-Value="_newPassword"/>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<BSLabel>Groups</BSLabel>
|
||||
<BSListGroup>
|
||||
<BSListGroupItem>
|
||||
<BSListGroup IsFlush="true">
|
||||
@foreach (var group in _userGroups) {
|
||||
<BSListGroupItem>
|
||||
<BSButton Color="BSColor.Danger" Size="Size.ExtraSmall" MarginEnd="Margins.Small" OnClick="() => RemoveGroup(group)">
|
||||
<HopIconDisplay Type="HopIconDisplay.HopIcon.Cross"/>
|
||||
</BSButton>
|
||||
|
||||
<span>@group.Name.Replace("group.", "")</span>
|
||||
</BSListGroupItem>
|
||||
}
|
||||
</BSListGroup>
|
||||
</BSListGroupItem>
|
||||
<BSListGroupItem>
|
||||
<div style="display: flex; gap: 20px">
|
||||
<BSInput InputType="InputType.Select" @bind-Value="_selectedGroup">
|
||||
<option selected>Select group</option>
|
||||
|
||||
@foreach (var group in _allGroups) {
|
||||
@if (_userGroups.All(g => g.Name != group.Name)) {
|
||||
<option value="@group.Name">@group.Name.Replace("group.", "")</option>
|
||||
}
|
||||
}
|
||||
</BSInput>
|
||||
<BSButton Color="BSColor.Secondary" OnClick="AddGroup">Add</BSButton>
|
||||
</div>
|
||||
</BSListGroupItem>
|
||||
</BSListGroup>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<BSLabel>Permissions</BSLabel>
|
||||
<BSListGroup>
|
||||
<BSListGroupItem>
|
||||
<BSListGroup IsFlush="true">
|
||||
@foreach (var perm in _user.Permissions.Where(perm => !perm.PermissionName.StartsWith("group."))) {
|
||||
<BSListGroupItem>
|
||||
<BSButton Color="BSColor.Danger" Size="Size.ExtraSmall" MarginEnd="Margins.Small" OnClick="() => RemovePermission(perm)">
|
||||
<HopIconDisplay Type="HopIconDisplay.HopIcon.Cross"/>
|
||||
</BSButton>
|
||||
|
||||
<span>@perm.PermissionName</span>
|
||||
</BSListGroupItem>
|
||||
}
|
||||
</BSListGroup>
|
||||
</BSListGroupItem>
|
||||
<BSListGroupItem>
|
||||
<div style="display: flex; gap: 20px">
|
||||
<BSInput InputType="InputType.Text" @bind-Value="_permissionToAdd"/>
|
||||
<BSButton Color="BSColor.Secondary" OnClick="AddPermission">Add</BSButton>
|
||||
</div>
|
||||
</BSListGroupItem>
|
||||
</BSListGroup>
|
||||
</div>
|
||||
</BSModalContent>
|
||||
<BSModalFooter>
|
||||
<BSButton Target="edit-user-modal">Cancel</BSButton>
|
||||
<BSButton IsSubmit="true" Color="BSColor.Primary">Save</BSButton>
|
||||
</BSModalFooter>
|
||||
</BSForm>
|
||||
</BSModal>
|
||||
|
||||
@inject IUserService Users
|
||||
@inject IPermissionService Permissions
|
||||
@inject SweetAlertService Alerts
|
||||
@inject ITokenContext Auth
|
||||
|
||||
@code {
|
||||
[Parameter] public Func<Task> ReloadPage { get; set; }
|
||||
|
||||
private BSModalBase _modal;
|
||||
private User _user;
|
||||
private string _newPassword;
|
||||
|
||||
private IList<PermissionGroup> _userGroups;
|
||||
private IList<PermissionGroup> _allGroups;
|
||||
private string _selectedGroup;
|
||||
private string _permissionToAdd;
|
||||
|
||||
public async Task ShowAsync(User user) {
|
||||
if (!(await Permissions.HasPermission(Security.AdminPermissions.EditUser, Auth.User.Id))) {
|
||||
await NoEditPermissions();
|
||||
return;
|
||||
}
|
||||
|
||||
_user = user;
|
||||
_userGroups = await Permissions.GetUserPermissionGroups(_user);
|
||||
_allGroups = await Permissions.GetPermissionGroups();
|
||||
await _modal.ShowAsync();
|
||||
}
|
||||
|
||||
private async Task AddGroup() {
|
||||
if (!(await Permissions.HasPermission(Security.AdminPermissions.EditUser, Auth.User.Id))) {
|
||||
await NoEditPermissions();
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_selectedGroup)) {
|
||||
await Alerts.FireAsync(new SweetAlertOptions {
|
||||
Title = "Select a group!",
|
||||
Icon = SweetAlertIcon.Error,
|
||||
ShowConfirmButton = true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var group = _allGroups.SingleOrDefault(group => group.Name == _selectedGroup);
|
||||
|
||||
await Permissions.AddPermission(_user, group?.Name);
|
||||
_userGroups.Add(group);
|
||||
|
||||
await Alerts.FireAsync(new SweetAlertOptions {
|
||||
Title = "Group added!",
|
||||
Icon = SweetAlertIcon.Success,
|
||||
Timer = 1500,
|
||||
ShowConfirmButton = false
|
||||
});
|
||||
}
|
||||
|
||||
private async Task RemoveGroup(PermissionGroup group) {
|
||||
if (!(await Permissions.HasPermission(Security.AdminPermissions.EditUser, Auth.User.Id))) {
|
||||
await NoEditPermissions();
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await Alerts.FireAsync(new SweetAlertOptions {
|
||||
Title = "Are you sure?",
|
||||
Icon = SweetAlertIcon.Warning,
|
||||
ConfirmButtonText = "Yes",
|
||||
ShowCancelButton = true,
|
||||
ShowConfirmButton = true
|
||||
});
|
||||
|
||||
if (result.IsConfirmed) {
|
||||
await Permissions.RemoveGroupFromUser(_user, group);
|
||||
_userGroups.Remove(group);
|
||||
StateHasChanged();
|
||||
|
||||
await Alerts.FireAsync(new SweetAlertOptions {
|
||||
Title = "Group removed!",
|
||||
Icon = SweetAlertIcon.Success,
|
||||
Timer = 1500,
|
||||
ShowConfirmButton = false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddPermission() {
|
||||
if (!(await Permissions.HasPermission(Security.AdminPermissions.EditUser, Auth.User.Id))) {
|
||||
await NoEditPermissions();
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_permissionToAdd)) {
|
||||
await Alerts.FireAsync(new SweetAlertOptions {
|
||||
Title = "Enter a permission name!",
|
||||
Icon = SweetAlertIcon.Error,
|
||||
ShowConfirmButton = true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await Permissions.AddPermission(_user, _permissionToAdd);
|
||||
_user.Permissions.Add(await Permissions.GetPermission(_permissionToAdd, _user));
|
||||
_permissionToAdd = "";
|
||||
|
||||
await Alerts.FireAsync(new SweetAlertOptions {
|
||||
Title = "Permission added!",
|
||||
Icon = SweetAlertIcon.Success,
|
||||
Timer = 1500,
|
||||
ShowConfirmButton = false
|
||||
});
|
||||
}
|
||||
|
||||
private async Task RemovePermission(Permission perm) {
|
||||
if (!(await Permissions.HasPermission(Security.AdminPermissions.EditUser, Auth.User.Id))) {
|
||||
await NoEditPermissions();
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await Alerts.FireAsync(new SweetAlertOptions {
|
||||
Title = "Are you sure?",
|
||||
Icon = SweetAlertIcon.Warning,
|
||||
ConfirmButtonText = "Yes",
|
||||
ShowCancelButton = true,
|
||||
ShowConfirmButton = true
|
||||
});
|
||||
|
||||
if (result.IsConfirmed) {
|
||||
await Permissions.RemovePermission(perm);
|
||||
_user.Permissions.Remove(perm);
|
||||
StateHasChanged();
|
||||
|
||||
await Alerts.FireAsync(new SweetAlertOptions {
|
||||
Title = "Permission removed!",
|
||||
Icon = SweetAlertIcon.Success,
|
||||
Timer = 1500,
|
||||
ShowConfirmButton = false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async void EditUser() {
|
||||
if (!(await Permissions.HasPermission(Security.AdminPermissions.EditUser, Auth.User.Id))) {
|
||||
await NoEditPermissions();
|
||||
return;
|
||||
}
|
||||
|
||||
string errorMessage = null;
|
||||
var validator = new RegisterData {
|
||||
Password = _newPassword,
|
||||
Email = _user.Email
|
||||
};
|
||||
|
||||
var allUsers = await Users.GetUsers();
|
||||
|
||||
if (allUsers.Any(user => user.Username == _user.Username && user.Id != _user.Id)) {
|
||||
errorMessage = "Username is already taken!";
|
||||
}
|
||||
else if (allUsers.Any(user => user.Email == _user.Email && user.Id != _user.Id)) {
|
||||
errorMessage = "E-Mail is already taken!";
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(_newPassword) && !validator.PasswordIsValid) {
|
||||
errorMessage = "The password needs to be at least 8 characters long!";
|
||||
}
|
||||
else if (!validator.EmailIsValid) {
|
||||
errorMessage = "Invalid E-Mail address!";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(errorMessage)) {
|
||||
await Alerts.FireAsync(new SweetAlertOptions {
|
||||
Title = "Something went wrong!",
|
||||
Text = errorMessage,
|
||||
Icon = SweetAlertIcon.Error,
|
||||
ShowConfirmButton = false,
|
||||
Timer = 1500
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await Users.UpdateUser(_user);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_newPassword)) {
|
||||
await Users.ChangePassword(_user, _newPassword);
|
||||
}
|
||||
|
||||
if (ReloadPage is not null)
|
||||
await ReloadPage.Invoke();
|
||||
|
||||
await Alerts.FireAsync(new SweetAlertOptions {
|
||||
Title = "User edited!",
|
||||
Icon = SweetAlertIcon.Success,
|
||||
Timer = 1500,
|
||||
ShowConfirmButton = false
|
||||
});
|
||||
}
|
||||
|
||||
private async Task NoEditPermissions() {
|
||||
await Alerts.FireAsync(new SweetAlertOptions {
|
||||
Title = "Unauthorized!",
|
||||
Text = "You don't have the required permissions to edit a user!",
|
||||
Icon = SweetAlertIcon.Error
|
||||
});
|
||||
}
|
||||
}
|
||||
49
src/HopFrame.Web/Components/AuthorizedView.razor
Normal file
49
src/HopFrame.Web/Components/AuthorizedView.razor
Normal file
@@ -0,0 +1,49 @@
|
||||
@using HopFrame.Security.Authorization
|
||||
@using HopFrame.Security.Claims
|
||||
@using Microsoft.AspNetCore.Http
|
||||
|
||||
@if (HandleComponent()) {
|
||||
@ChildContent
|
||||
}
|
||||
|
||||
@inject ITokenContext Auth
|
||||
@inject IHttpContextAccessor HttpAccessor
|
||||
@inject NavigationManager Navigator
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string[] Permissions { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string Permission { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string RedirectIfUnauthorized { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment ChildContent { get; set; }
|
||||
|
||||
private bool IsAuthorized() {
|
||||
if (!Auth.IsAuthenticated) return false;
|
||||
if ((Permissions == null || Permissions.Length == 0) && string.IsNullOrEmpty(Permission)) return true;
|
||||
|
||||
Permissions ??= [];
|
||||
var perms = new List<string>(Permissions);
|
||||
if (!string.IsNullOrEmpty(Permission)) perms.Add(Permission);
|
||||
|
||||
var permissions = HttpAccessor.HttpContext?.User.GetPermissions();
|
||||
if (!perms.All(perm => PermissionValidator.IncludesPermission(perm, permissions))) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool HandleComponent() {
|
||||
var authorized = IsAuthorized();
|
||||
|
||||
if (authorized == false && !string.IsNullOrEmpty(RedirectIfUnauthorized)) {
|
||||
Navigator.NavigateTo(RedirectIfUnauthorized, true);
|
||||
}
|
||||
|
||||
return authorized;
|
||||
}
|
||||
}
|
||||
33
src/HopFrame.Web/HopFrame.Web.csproj
Normal file
33
src/HopFrame.Web/HopFrame.Web.csproj
Normal file
@@ -0,0 +1,33 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>disable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\HopFrame.Database\HopFrame.Database.csproj" />
|
||||
<ProjectReference Include="..\HopFrame.Security\HopFrame.Security.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BlazorStrap" Version="5.2.100.61524" />
|
||||
<PackageReference Include="BlazorStrap.V5" Version="5.2.100" />
|
||||
<PackageReference Include="CurrieTechnologies.Razor.SweetAlert2" Version="5.6.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components" Version="8.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.7" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="README.md" Pack="true" PackagePath="\"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
8
src/HopFrame.Web/Model/NavigationItem.cs
Normal file
8
src/HopFrame.Web/Model/NavigationItem.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace HopFrame.Web.Model;
|
||||
|
||||
public sealed class NavigationItem {
|
||||
public string Name { get; set; }
|
||||
public string Url { get; set; }
|
||||
public string Permission { get; set; }
|
||||
public string Description { get; set; }
|
||||
}
|
||||
7
src/HopFrame.Web/Model/PermissionGroupAdd.cs
Normal file
7
src/HopFrame.Web/Model/PermissionGroupAdd.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using HopFrame.Database.Models;
|
||||
|
||||
namespace HopFrame.Web.Model;
|
||||
|
||||
internal sealed class PermissionGroupAdd : PermissionGroup {
|
||||
public string GroupName { get; set; }
|
||||
}
|
||||
11
src/HopFrame.Web/Model/RegisterData.cs
Normal file
11
src/HopFrame.Web/Model/RegisterData.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using HopFrame.Security.Models;
|
||||
|
||||
namespace HopFrame.Web.Model;
|
||||
|
||||
internal class RegisterData : UserRegister {
|
||||
public string RepeatedPassword { get; set; }
|
||||
|
||||
public bool PasswordsMatch => Password == RepeatedPassword;
|
||||
public bool PasswordIsValid => Password?.Length >= 8;
|
||||
public bool EmailIsValid => Email?.Contains('@') == true && Email?.Contains('.') == true && Email?.EndsWith('.') == false;
|
||||
}
|
||||
5
src/HopFrame.Web/Model/UserAdd.cs
Normal file
5
src/HopFrame.Web/Model/UserAdd.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
namespace HopFrame.Web.Model;
|
||||
|
||||
internal sealed class UserAdd : RegisterData {
|
||||
public string Group { get; set; }
|
||||
}
|
||||
33
src/HopFrame.Web/Pages/Administration/AdminDashboard.razor
Normal file
33
src/HopFrame.Web/Pages/Administration/AdminDashboard.razor
Normal file
@@ -0,0 +1,33 @@
|
||||
@page "/administration"
|
||||
@rendermode InteractiveServer
|
||||
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using BlazorStrap
|
||||
@using HopFrame.Web.Pages.Administration.Layout
|
||||
@using BlazorStrap.V5
|
||||
@using HopFrame.Web.Components
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@layout AdminLayout
|
||||
|
||||
<PageTitle>Admin Dashboard</PageTitle>
|
||||
|
||||
<BSContainer>
|
||||
<BSRow Justify="Justify.Center">
|
||||
@foreach (var view in AdminMenu.Subpages) {
|
||||
<AuthorizedView Permission="@view.Permission">
|
||||
<BSCol Column="4" style="margin-bottom: 10px">
|
||||
<BSCard CardType="CardType.Card" Color="BSColor.Dark" style="min-height: 200px">
|
||||
<BSCard CardType="CardType.Body" style="display: flex; flex-direction: column">
|
||||
<BSCard CardType="CardType.Title">@view.Name</BSCard>
|
||||
<BSCard CardType="CardType.Subtitle"><span style="color: gray">@view.Permission</span></BSCard>
|
||||
<BSCard CardType="CardType.Text">@view.Description</BSCard>
|
||||
<BSButton IsOutlined="true" MarginTop="Margins.Auto" style="width: max-content; align-self: center" OnClick="() => Navigator.NavigateTo(view.Url, true)" Color="BSColor.Light">Open</BSButton>
|
||||
</BSCard>
|
||||
</BSCard>
|
||||
</BSCol>
|
||||
</AuthorizedView>
|
||||
}
|
||||
</BSRow>
|
||||
</BSContainer>
|
||||
|
||||
@inject NavigationManager Navigator
|
||||
67
src/HopFrame.Web/Pages/Administration/AdminLogin.razor
Normal file
67
src/HopFrame.Web/Pages/Administration/AdminLogin.razor
Normal file
@@ -0,0 +1,67 @@
|
||||
@page "/administration/login"
|
||||
@layout EmptyLayout
|
||||
|
||||
@using BlazorStrap
|
||||
@using BlazorStrap.V5
|
||||
@using HopFrame.Security.Models
|
||||
@using HopFrame.Web.Pages.Administration.Layout
|
||||
@using HopFrame.Web.Services
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
|
||||
<PageTitle>Login</PageTitle>
|
||||
|
||||
<div class="login-wrapper">
|
||||
<EditForm Model="UserLogin" OnValidSubmit="Login" FormName="login-form">
|
||||
<div class="field-wrapper">
|
||||
<h3>Login</h3>
|
||||
<div class="mb-3">
|
||||
<BSLabel>E-Mail address</BSLabel>
|
||||
<InputText type="email" class="form-control" required @bind-Value="UserLogin.Email"/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<BSLabel>Password</BSLabel>
|
||||
<InputText type="password" class="form-control" required @bind-Value="UserLogin.Password"/>
|
||||
</div>
|
||||
<BSButton Color="BSColor.Primary" IsSubmit="true">Login</BSButton>
|
||||
|
||||
@if (_hasError) {
|
||||
<BSAlert Color="BSColor.Danger" style="margin-top: 16px; margin-bottom: 0">Email or password does not match any account!</BSAlert>
|
||||
}
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
|
||||
@inject IAuthService Auth
|
||||
@inject NavigationManager Navigator
|
||||
|
||||
@code {
|
||||
[SupplyParameterFromForm]
|
||||
private UserLogin UserLogin { get; set; }
|
||||
|
||||
[SupplyParameterFromQuery(Name = "redirect")]
|
||||
private string RedirectAfter { get; set; }
|
||||
|
||||
private const string DefaultRedirect = "/administration";
|
||||
|
||||
private bool _hasError = false;
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
UserLogin ??= new();
|
||||
|
||||
if (await Auth.IsLoggedIn()) {
|
||||
await Auth.Logout();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Login() {
|
||||
var result = await Auth.Login(UserLogin);
|
||||
|
||||
if (!result) {
|
||||
_hasError = true;
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.NavigateTo(string.IsNullOrEmpty(RedirectAfter) ? DefaultRedirect : RedirectAfter, true);
|
||||
}
|
||||
}
|
||||
15
src/HopFrame.Web/Pages/Administration/AdminLogin.razor.css
Normal file
15
src/HopFrame.Web/Pages/Administration/AdminLogin.razor.css
Normal file
@@ -0,0 +1,15 @@
|
||||
.login-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.field-wrapper {
|
||||
margin-top: 25vh;
|
||||
min-width: 30vw;
|
||||
|
||||
padding: 30px;
|
||||
border: 2px solid #ced4da;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
}
|
||||
191
src/HopFrame.Web/Pages/Administration/GroupsPage.razor
Normal file
191
src/HopFrame.Web/Pages/Administration/GroupsPage.razor
Normal file
@@ -0,0 +1,191 @@
|
||||
@page "/administration/groups"
|
||||
@rendermode InteractiveServer
|
||||
@layout AdminLayout
|
||||
|
||||
@using System.Globalization
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using BlazorStrap
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using HopFrame.Web.Components
|
||||
@using HopFrame.Web.Components.Administration
|
||||
@using BlazorStrap.V5
|
||||
@using CurrieTechnologies.Razor.SweetAlert2
|
||||
@using HopFrame.Database.Models
|
||||
@using HopFrame.Security.Claims
|
||||
@using HopFrame.Security.Services
|
||||
@using HopFrame.Web.Pages.Administration.Layout
|
||||
|
||||
<PageTitle>Groups</PageTitle>
|
||||
<AuthorizedView Permission="@Security.AdminPermissions.ViewGroups" RedirectIfUnauthorized="administration/login?redirect=/administration/groups"/>
|
||||
|
||||
<GroupAddModal ReloadPage="Reload" @ref="_groupAddModal"/>
|
||||
|
||||
<div class="title">
|
||||
<h3>
|
||||
Groups administration
|
||||
<span class="reload" @onclick="Reload">
|
||||
<HopIconDisplay Type="HopIconDisplay.HopIcon.Reload"/>
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
<form class="d-flex" role="search" id="search" @onsubmit="Search">
|
||||
<input class="form-control me-2 input-dark" type="search" placeholder="Search" aria-label="Search" @bind="_searchText">
|
||||
<BSButton Color="BSColor.Success" IsOutlined="true" type="submit">Search</BSButton>
|
||||
</form>
|
||||
<AuthorizedView Permission="@Security.AdminPermissions.AddGroup">
|
||||
<BSButton IsSubmit="false" Color="BSColor.Success" Target="add-user" OnClick="() => _groupAddModal.ShowAsync()">Add Group</BSButton>
|
||||
</AuthorizedView>
|
||||
</div>
|
||||
|
||||
<BSTable IsStriped="true" IsHoverable="true" IsDark="true" Color="BSColor.Dark">
|
||||
<BSTHead>
|
||||
<BSTR>
|
||||
<BSTD>
|
||||
<span class="sorter" @onclick="() => OrderBy(OrderType.Name)">Name</span>
|
||||
@if (_currentOrder == OrderType.Name) {
|
||||
<HopIconDisplay Type="_currentOrderDirection == OrderDirection.Desc ? HopIconDisplay.HopIcon.ArrowDown : HopIconDisplay.HopIcon.ArrowUp"/>
|
||||
}
|
||||
</BSTD>
|
||||
<BSTD>Description</BSTD>
|
||||
<BSTD>Default</BSTD>
|
||||
<BSTD>
|
||||
<span class="sorter" @onclick="() => OrderBy(OrderType.Created)">Created</span>
|
||||
@if (_currentOrder == OrderType.Created) {
|
||||
<HopIconDisplay Type="_currentOrderDirection == OrderDirection.Desc ? HopIconDisplay.HopIcon.ArrowDown : HopIconDisplay.HopIcon.ArrowUp"/>
|
||||
}
|
||||
</BSTD>
|
||||
|
||||
@if (_hasEditPrivileges || _hasDeletePrivileges) {
|
||||
<BSTD>Actions</BSTD>
|
||||
}
|
||||
</BSTR>
|
||||
</BSTHead>
|
||||
|
||||
<BSTBody>
|
||||
@foreach (var group in _groups) {
|
||||
<BSTR>
|
||||
<BSTD Class="bold">@group.Name.Replace("group.", "")</BSTD>
|
||||
<BSTD>@group.Description</BSTD>
|
||||
<BSTD>
|
||||
@if (group.IsDefaultGroup) {
|
||||
<span>Yes</span>
|
||||
}
|
||||
else {
|
||||
<span>No</span>
|
||||
}
|
||||
</BSTD>
|
||||
<BSTD>@group.CreatedAt</BSTD>
|
||||
|
||||
@if (_hasEditPrivileges || _hasDeletePrivileges) {
|
||||
<BSTD>
|
||||
<BSButtonGroup>
|
||||
@if (_hasEditPrivileges) {
|
||||
<BSButton Color="BSColor.Warning" OnClick="() => _groupAddModal.ShowAsync(group)">Edit</BSButton>
|
||||
}
|
||||
|
||||
@if (_hasDeletePrivileges) {
|
||||
<BSButton Color="BSColor.Danger" OnClick="() => Delete(group)">Delete</BSButton>
|
||||
}
|
||||
</BSButtonGroup>
|
||||
</BSTD>
|
||||
}
|
||||
</BSTR>
|
||||
}
|
||||
</BSTBody>
|
||||
</BSTable>
|
||||
|
||||
@inject IPermissionService Permissions
|
||||
@inject ITokenContext Auth
|
||||
@inject SweetAlertService Alerts
|
||||
|
||||
@code {
|
||||
private IList<PermissionGroup> _groups = new List<PermissionGroup>();
|
||||
|
||||
private bool _hasEditPrivileges = false;
|
||||
private bool _hasDeletePrivileges = false;
|
||||
private string _searchText;
|
||||
private OrderType _currentOrder = OrderType.None;
|
||||
private OrderDirection _currentOrderDirection = OrderDirection.Asc;
|
||||
|
||||
private GroupAddModal _groupAddModal;
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
_groups = await Permissions.GetPermissionGroups();
|
||||
|
||||
_hasEditPrivileges = await Permissions.HasPermission(Security.AdminPermissions.EditGroup, Auth.User.Id);
|
||||
_hasDeletePrivileges = await Permissions.HasPermission(Security.AdminPermissions.DeleteGroup, Auth.User.Id);
|
||||
}
|
||||
|
||||
private async Task Reload() {
|
||||
_groups = new List<PermissionGroup>();
|
||||
|
||||
_groups = await Permissions.GetPermissionGroups();
|
||||
|
||||
OrderBy(_currentOrder, false);
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task Search() {
|
||||
var groups = await Permissions.GetPermissionGroups();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_searchText)) {
|
||||
groups = groups
|
||||
.Where(group => group.Name.Contains(_searchText) ||
|
||||
group.Description?.Contains(_searchText) == true ||
|
||||
group.CreatedAt.ToString(CultureInfo.InvariantCulture).Contains(_searchText) ||
|
||||
group.Permissions.Any(perm => perm.PermissionName.Contains(_searchText)))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
_groups = groups;
|
||||
OrderBy(_currentOrder, false);
|
||||
}
|
||||
|
||||
private void OrderBy(OrderType type, bool changeDir = true) {
|
||||
if (_currentOrder == type && changeDir) _currentOrderDirection = (OrderDirection)(((byte)_currentOrderDirection + 1) % 2);
|
||||
if (_currentOrder != type) _currentOrderDirection = OrderDirection.Asc;
|
||||
|
||||
if (type == OrderType.Name) {
|
||||
_groups = _currentOrderDirection == OrderDirection.Asc ? _groups.OrderBy(group => group.Name).ToList() : _groups.OrderByDescending(group => group.Name).ToList();
|
||||
}
|
||||
else if (type == OrderType.Created) {
|
||||
_groups = _currentOrderDirection == OrderDirection.Asc ? _groups.OrderBy(group => group.CreatedAt).ToList() : _groups.OrderByDescending(group => group.CreatedAt).ToList();
|
||||
}
|
||||
|
||||
_currentOrder = type;
|
||||
}
|
||||
|
||||
private async Task Delete(PermissionGroup group) {
|
||||
var result = await Alerts.FireAsync(new SweetAlertOptions {
|
||||
Title = "Are you sure?",
|
||||
Text = "You won't be able to revert this!",
|
||||
Icon = SweetAlertIcon.Warning,
|
||||
ConfirmButtonText = "Yes",
|
||||
ShowCancelButton = true,
|
||||
ShowConfirmButton = true
|
||||
});
|
||||
|
||||
if (result.IsConfirmed) {
|
||||
await Permissions.DeletePermissionGroup(group);
|
||||
await Reload();
|
||||
|
||||
await Alerts.FireAsync(new SweetAlertOptions {
|
||||
Title = "Deleted!",
|
||||
Icon = SweetAlertIcon.Success,
|
||||
Timer = 1500,
|
||||
ShowConfirmButton = false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private enum OrderType {
|
||||
None,
|
||||
Name,
|
||||
Created
|
||||
}
|
||||
|
||||
private enum OrderDirection : byte {
|
||||
Asc = 0,
|
||||
Desc = 1
|
||||
}
|
||||
}
|
||||
26
src/HopFrame.Web/Pages/Administration/GroupsPage.razor.css
Normal file
26
src/HopFrame.Web/Pages/Administration/GroupsPage.razor.css
Normal file
@@ -0,0 +1,26 @@
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#search {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
th, h3 {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.reload, .sorter {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
@using HopFrame.Web.Components
|
||||
@using BlazorStrap.V5
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<AuthorizedView Permission="@Security.AdminPermissions.IsAdmin" RedirectIfUnauthorized="administration/login" />
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
|
||||
<div class="page" style="background-color: #2c3034; position: relative; min-height: 100vh">
|
||||
<nav>
|
||||
<AdminMenu/>
|
||||
</nav>
|
||||
|
||||
<main style="padding-top: 60px">
|
||||
<article class="content px-4">
|
||||
@Body
|
||||
<BSCore/>
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="_content/CurrieTechnologies.Razor.SweetAlert2/sweetAlert2.min.js"></script>
|
||||
<script src="_content/BlazorStrap/popper.min.js"></script>
|
||||
85
src/HopFrame.Web/Pages/Administration/Layout/AdminMenu.razor
Normal file
85
src/HopFrame.Web/Pages/Administration/Layout/AdminMenu.razor
Normal file
@@ -0,0 +1,85 @@
|
||||
@rendermode InteractiveServer
|
||||
|
||||
@using BlazorStrap
|
||||
@using BlazorStrap.V5
|
||||
@using HopFrame.Security.Claims
|
||||
@using HopFrame.Web.Services
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using HopFrame.Web.Components.Administration
|
||||
@using HopFrame.Web.Model
|
||||
@using HopFrame.Web.Components
|
||||
|
||||
|
||||
<BSNavbar Color="BSColor.Dark" IsDark="true" IsFixedTop="true">
|
||||
<BSContainer Container="Container.Fluid">
|
||||
<BSNavbarBrand>
|
||||
HopFrame
|
||||
</BSNavbarBrand>
|
||||
<BSCollapse IsInNavbar="true">
|
||||
<Toggler>
|
||||
<BSNavbarToggle/>
|
||||
</Toggler>
|
||||
<Content>
|
||||
<BSNav MarginEnd="Margins.Auto" MarginBottom="Margins.Small" Class="mb-lg-0">
|
||||
<BSNavItem IsActive="IsDashboardActive()" OnClick="NavigateToDashboard">Dashboard</BSNavItem>
|
||||
|
||||
@foreach (var nav in Subpages) {
|
||||
<AuthorizedView Permission="@nav.Permission">
|
||||
<BSNavItem IsActive="IsNavItemActive(nav.Url)" OnClick="() => Navigate(nav.Url)">@nav.Name</BSNavItem>
|
||||
</AuthorizedView>
|
||||
}
|
||||
</BSNav>
|
||||
|
||||
<span style="margin-left: auto; line-height: 100%; color: white">
|
||||
logged in as @Context?.User.Username
|
||||
</span>
|
||||
<BSButton DataId="logout" Size="Size.ExtraSmall" OnClick="Logout" Color="BSColor.Dark">
|
||||
<HopIconDisplay Type="HopIconDisplay.HopIcon.Logout"/>
|
||||
</BSButton>
|
||||
<BSTooltip Placement="Placement.Bottom" Target="logout" ContentAlwaysRendered="false">logout</BSTooltip>
|
||||
</Content>
|
||||
</BSCollapse>
|
||||
</BSContainer>
|
||||
</BSNavbar>
|
||||
|
||||
|
||||
@inject NavigationManager Navigator
|
||||
@inject ITokenContext Context
|
||||
@inject IAuthService Auth
|
||||
|
||||
@code {
|
||||
public static IList<NavigationItem> Subpages = new List<NavigationItem> {
|
||||
new () {
|
||||
Name = "Users",
|
||||
Url = "administration/users",
|
||||
Description = "On this page you can manage all user accounts.",
|
||||
Permission = Security.AdminPermissions.ViewUsers
|
||||
},
|
||||
new () {
|
||||
Name = "Groups",
|
||||
Url = "administration/groups",
|
||||
Description = "On this page you can view, create, edit and delete permission groups.",
|
||||
Permission = Security.AdminPermissions.ViewGroups
|
||||
}
|
||||
};
|
||||
|
||||
private bool IsNavItemActive(string element) {
|
||||
return Navigator.Uri.Contains(element);
|
||||
}
|
||||
|
||||
private bool IsDashboardActive() {
|
||||
return Navigator.Uri.TrimEnd('/').EndsWith("administration");
|
||||
}
|
||||
|
||||
private void NavigateToDashboard() {
|
||||
Navigate("administration");
|
||||
}
|
||||
|
||||
private void Navigate(string url) {
|
||||
Navigator.NavigateTo(url, true);
|
||||
}
|
||||
|
||||
private void Logout() {
|
||||
Navigator.NavigateTo("administration/login", true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
@using BlazorStrap.V5
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
|
||||
@Body
|
||||
<BSCore/>
|
||||
|
||||
<script src="_content/BlazorStrap/popper.min.js"></script>
|
||||
221
src/HopFrame.Web/Pages/Administration/UsersPage.razor
Normal file
221
src/HopFrame.Web/Pages/Administration/UsersPage.razor
Normal file
@@ -0,0 +1,221 @@
|
||||
@page "/administration/users"
|
||||
@rendermode InteractiveServer
|
||||
@layout AdminLayout
|
||||
|
||||
@using System.Globalization
|
||||
@using BlazorStrap
|
||||
@using CurrieTechnologies.Razor.SweetAlert2
|
||||
@using HopFrame.Database.Models
|
||||
@using HopFrame.Security.Claims
|
||||
@using HopFrame.Security.Services
|
||||
@using HopFrame.Web.Pages.Administration.Layout
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using HopFrame.Web.Components
|
||||
@using BlazorStrap.V5
|
||||
@using HopFrame.Web.Components.Administration
|
||||
|
||||
<PageTitle>Users</PageTitle>
|
||||
<AuthorizedView Permission="@Security.AdminPermissions.ViewUsers" RedirectIfUnauthorized="administration/login?redirect=/administration/users"/>
|
||||
|
||||
<UserAddModal @ref="_userAddModal" ReloadPage="Reload"/>
|
||||
<UserEditModal @ref="_userEditModal" ReloadPage="Reload"/>
|
||||
|
||||
<div class="title">
|
||||
<h3>
|
||||
Users administration
|
||||
<span class="reload" @onclick="Reload">
|
||||
<HopIconDisplay Type="HopIconDisplay.HopIcon.Reload"/>
|
||||
</span>
|
||||
</h3>
|
||||
|
||||
<form class="d-flex" role="search" @onsubmit="Search" id="search">
|
||||
<input class="form-control me-2 input-dark" type="search" placeholder="Search" aria-label="Search" @bind="_searchText">
|
||||
<BSButton Color="BSColor.Success" IsOutlined="true" type="submit">Search</BSButton>
|
||||
</form>
|
||||
<AuthorizedView Permission="@Security.AdminPermissions.AddUser">
|
||||
<BSButton IsSubmit="false" Color="BSColor.Success" Target="add-user" OnClick="() => _userAddModal.ShowAsync()">Add User</BSButton>
|
||||
</AuthorizedView>
|
||||
</div>
|
||||
|
||||
<BSTable IsStriped="true" IsHoverable="true" IsDark="true" Color="BSColor.Dark">
|
||||
<BSTHead>
|
||||
<BSTR>
|
||||
<BSTD>#</BSTD>
|
||||
<BSTD>
|
||||
<span class="sorter" @onclick="() => OrderBy(OrderType.Email)">E-Mail</span>
|
||||
@if (_currentOrder == OrderType.Email) {
|
||||
<HopIconDisplay Type="_currentOrderDirection == OrderDirection.Desc ? HopIconDisplay.HopIcon.ArrowDown : HopIconDisplay.HopIcon.ArrowUp"/>
|
||||
}
|
||||
</BSTD>
|
||||
<BSTD>
|
||||
<span class="sorter" @onclick="() => OrderBy(OrderType.Username)">Username</span>
|
||||
@if (_currentOrder == OrderType.Username) {
|
||||
<HopIconDisplay Type="_currentOrderDirection == OrderDirection.Desc ? HopIconDisplay.HopIcon.ArrowDown : HopIconDisplay.HopIcon.ArrowUp"/>
|
||||
}
|
||||
</BSTD>
|
||||
<BSTD>
|
||||
<span class="sorter" @onclick="() => OrderBy(OrderType.Registered)">Registered</span>
|
||||
@if (_currentOrder == OrderType.Registered) {
|
||||
<HopIconDisplay Type="_currentOrderDirection == OrderDirection.Desc ? HopIconDisplay.HopIcon.ArrowDown : HopIconDisplay.HopIcon.ArrowUp"/>
|
||||
}
|
||||
</BSTD>
|
||||
<BSTD>Primary Group</BSTD>
|
||||
|
||||
@if (_hasEditPrivileges || _hasDeletePrivileges) {
|
||||
<BSTD>Actions</BSTD>
|
||||
}
|
||||
</BSTR>
|
||||
</BSTHead>
|
||||
|
||||
<BSTBody>
|
||||
@foreach (var user in _users) {
|
||||
<BSTR>
|
||||
<BSTD class="bold">@user.Id</BSTD>
|
||||
<BSTD>@user.Email</BSTD>
|
||||
<BSTD>@user.Username</BSTD>
|
||||
<BSTD>@user.CreatedAt</BSTD>
|
||||
<BSTD>@GetFriendlyGroupName(user)</BSTD>
|
||||
|
||||
@if (_hasEditPrivileges || _hasDeletePrivileges) {
|
||||
<BSTD>
|
||||
<BSButtonGroup>
|
||||
@if (_hasEditPrivileges) {
|
||||
<BSButton Color="BSColor.Warning" OnClick="() => _userEditModal.ShowAsync(user)">Edit</BSButton>
|
||||
}
|
||||
|
||||
@if (_hasDeletePrivileges) {
|
||||
<BSButton Color="BSColor.Danger" OnClick="() => Delete(user)">Delete</BSButton>
|
||||
}
|
||||
</BSButtonGroup>
|
||||
</BSTD>
|
||||
}
|
||||
</BSTR>
|
||||
}
|
||||
</BSTBody>
|
||||
</BSTable>
|
||||
|
||||
@inject IUserService UserService
|
||||
@inject IPermissionService PermissionsService
|
||||
@inject SweetAlertService Alerts
|
||||
@inject ITokenContext Auth
|
||||
|
||||
@code {
|
||||
private IList<User> _users = new List<User>();
|
||||
private IDictionary<Guid, PermissionGroup> _userGroups = new Dictionary<Guid, PermissionGroup>();
|
||||
|
||||
private OrderType _currentOrder = OrderType.None;
|
||||
private OrderDirection _currentOrderDirection = OrderDirection.Asc;
|
||||
|
||||
private string _searchText;
|
||||
|
||||
private bool _hasEditPrivileges = false;
|
||||
private bool _hasDeletePrivileges = false;
|
||||
|
||||
private UserAddModal _userAddModal;
|
||||
private UserEditModal _userEditModal;
|
||||
|
||||
protected override async Task OnInitializedAsync() {
|
||||
_users = await UserService.GetUsers();
|
||||
|
||||
foreach (var user in _users) {
|
||||
var groups = await PermissionsService.GetUserPermissionGroups(user);
|
||||
_userGroups.Add(user.Id, groups.LastOrDefault());
|
||||
}
|
||||
|
||||
_hasEditPrivileges = await PermissionsService.HasPermission(Security.AdminPermissions.EditUser, Auth.User.Id);
|
||||
_hasDeletePrivileges = await PermissionsService.HasPermission(Security.AdminPermissions.DeleteUser, Auth.User.Id);
|
||||
}
|
||||
|
||||
private async Task Reload() {
|
||||
_users = new List<User>();
|
||||
_userGroups = new Dictionary<Guid, PermissionGroup>();
|
||||
|
||||
_users = await UserService.GetUsers();
|
||||
|
||||
foreach (var user in _users) {
|
||||
var groups = await PermissionsService.GetUserPermissionGroups(user);
|
||||
_userGroups.Add(user.Id, groups.LastOrDefault());
|
||||
}
|
||||
|
||||
OrderBy(_currentOrder, false);
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task Search() {
|
||||
var users = await UserService.GetUsers();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_searchText)) {
|
||||
users = users
|
||||
.Where(user =>
|
||||
user.Email.Contains(_searchText) ||
|
||||
user.Username.Contains(_searchText) ||
|
||||
user.Id.ToString().Contains(_searchText) ||
|
||||
user.CreatedAt.ToString(CultureInfo.InvariantCulture).Contains(_searchText) ||
|
||||
_userGroups[user.Id]?.Name.Contains(_searchText) == true)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
_users = users;
|
||||
OrderBy(_currentOrder, false);
|
||||
}
|
||||
|
||||
private string GetFriendlyGroupName(User user) {
|
||||
var group = _userGroups[user.Id];
|
||||
if (group is null) return null;
|
||||
|
||||
return group.Name.Replace("group.", "");
|
||||
}
|
||||
|
||||
private void OrderBy(OrderType type, bool changeDir = true) {
|
||||
if (_currentOrder == type && changeDir) _currentOrderDirection = (OrderDirection)(((byte)_currentOrderDirection + 1) % 2);
|
||||
if (_currentOrder != type) _currentOrderDirection = OrderDirection.Asc;
|
||||
|
||||
if (type == OrderType.Email) {
|
||||
_users = _currentOrderDirection == OrderDirection.Asc ? _users.OrderBy(user => user.Email).ToList() : _users.OrderByDescending(user => user.Email).ToList();
|
||||
}
|
||||
else if (type == OrderType.Username) {
|
||||
_users = _currentOrderDirection == OrderDirection.Asc ? _users.OrderBy(user => user.Username).ToList() : _users.OrderByDescending(user => user.Username).ToList();
|
||||
}
|
||||
else if (type == OrderType.Registered) {
|
||||
_users = _currentOrderDirection == OrderDirection.Asc ? _users.OrderBy(user => user.CreatedAt).ToList() : _users.OrderByDescending(user => user.CreatedAt).ToList();
|
||||
}
|
||||
|
||||
_currentOrder = type;
|
||||
}
|
||||
|
||||
private async Task Delete(User user) {
|
||||
var result = await Alerts.FireAsync(new SweetAlertOptions {
|
||||
Title = "Are you sure?",
|
||||
Text = "You won't be able to revert this!",
|
||||
Icon = SweetAlertIcon.Warning,
|
||||
ConfirmButtonText = "Yes",
|
||||
ShowCancelButton = true,
|
||||
ShowConfirmButton = true
|
||||
});
|
||||
|
||||
if (result.IsConfirmed) {
|
||||
await UserService.DeleteUser(user);
|
||||
await Reload();
|
||||
|
||||
await Alerts.FireAsync(new SweetAlertOptions {
|
||||
Title = "Deleted!",
|
||||
Icon = SweetAlertIcon.Success,
|
||||
Timer = 1500,
|
||||
ShowConfirmButton = false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private enum OrderType {
|
||||
None,
|
||||
Email,
|
||||
Username,
|
||||
Registered
|
||||
}
|
||||
|
||||
private enum OrderDirection : byte {
|
||||
Asc = 0,
|
||||
Desc = 1
|
||||
}
|
||||
}
|
||||
26
src/HopFrame.Web/Pages/Administration/UsersPage.razor.css
Normal file
26
src/HopFrame.Web/Pages/Administration/UsersPage.razor.css
Normal file
@@ -0,0 +1,26 @@
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#search {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
th, h3 {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.reload, .sorter {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
43
src/HopFrame.Web/README.md
Normal file
43
src/HopFrame.Web/README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# HopFrame Web module
|
||||
This module contains useful helpers for Blazor Apps and an Admin Dashboard.
|
||||
|
||||
## How to use the Blazor API
|
||||
|
||||
1. Add the HopFrame.Web library to your project
|
||||
|
||||
```
|
||||
dotnet add package HopFrame.Web
|
||||
```
|
||||
|
||||
2. Create a DbContext that inherits the ``HopDbContext`` and add a data source
|
||||
|
||||
```csharp
|
||||
public class DatabaseContext : HopDbContextBase {
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
|
||||
optionsBuilder.UseSqlite("...");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. Add the DbContext and HopFrame to your services
|
||||
|
||||
```csharp
|
||||
builder.Services.AddDbContext<DatabaseContext>();
|
||||
builder.Services.AddHopFrame<DatabaseContext>();
|
||||
```
|
||||
|
||||
4. Add the authentication middleware to your app
|
||||
|
||||
```csharp
|
||||
app.UseMiddleware<AuthMiddleware>();
|
||||
```
|
||||
|
||||
5. Add the HopFrame pages to your Razor components
|
||||
|
||||
```csharp
|
||||
app.MapRazorComponents<App>()
|
||||
.AddHopFrameAdminPages()
|
||||
.AddInteractiveServerRenderMode();
|
||||
```
|
||||
33
src/HopFrame.Web/ServiceCollectionExtensions.cs
Normal file
33
src/HopFrame.Web/ServiceCollectionExtensions.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using BlazorStrap;
|
||||
using CurrieTechnologies.Razor.SweetAlert2;
|
||||
using HopFrame.Database;
|
||||
using HopFrame.Security.Authentication;
|
||||
using HopFrame.Web.Services;
|
||||
using HopFrame.Web.Services.Implementation;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace HopFrame.Web;
|
||||
|
||||
public static class ServiceCollectionExtensions {
|
||||
public static IServiceCollection AddHopFrame<TDbContext>(this IServiceCollection services) where TDbContext : HopDbContextBase {
|
||||
services.AddHttpClient();
|
||||
services.AddScoped<IAuthService, AuthService<TDbContext>>();
|
||||
services.AddTransient<AuthMiddleware>();
|
||||
|
||||
// Component library's
|
||||
services.AddSweetAlert2();
|
||||
services.AddBlazorStrap();
|
||||
|
||||
services.AddHopFrameAuthentication<TDbContext>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static RazorComponentsEndpointConventionBuilder AddHopFrameAdminPages(this RazorComponentsEndpointConventionBuilder builder) {
|
||||
return builder
|
||||
.DisableAntiforgery()
|
||||
.AddAdditionalAssemblies(typeof(ServiceCollectionExtensions).Assembly)
|
||||
.AddInteractiveServerRenderMode();
|
||||
}
|
||||
}
|
||||
13
src/HopFrame.Web/Services/IAuthService.cs
Normal file
13
src/HopFrame.Web/Services/IAuthService.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using HopFrame.Database.Models.Entries;
|
||||
using HopFrame.Security.Models;
|
||||
|
||||
namespace HopFrame.Web.Services;
|
||||
|
||||
public interface IAuthService {
|
||||
Task Register(UserRegister register);
|
||||
Task<bool> Login(UserLogin login);
|
||||
Task Logout();
|
||||
|
||||
Task<TokenEntry> RefreshLogin();
|
||||
Task<bool> IsLoggedIn();
|
||||
}
|
||||
167
src/HopFrame.Web/Services/Implementation/AuthService.cs
Normal file
167
src/HopFrame.Web/Services/Implementation/AuthService.cs
Normal file
@@ -0,0 +1,167 @@
|
||||
using HopFrame.Database;
|
||||
using HopFrame.Database.Models.Entries;
|
||||
using HopFrame.Security.Authentication;
|
||||
using HopFrame.Security.Claims;
|
||||
using HopFrame.Security.Models;
|
||||
using HopFrame.Security.Services;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace HopFrame.Web.Services.Implementation;
|
||||
|
||||
internal class AuthService<TDbContext>(
|
||||
IUserService userService,
|
||||
IHttpContextAccessor httpAccessor,
|
||||
TDbContext context)
|
||||
: IAuthService where TDbContext : HopDbContextBase {
|
||||
|
||||
public async Task Register(UserRegister register) {
|
||||
var user = await userService.AddUser(register);
|
||||
if (user is null) return;
|
||||
|
||||
var refreshToken = new TokenEntry {
|
||||
CreatedAt = DateTime.Now,
|
||||
Token = Guid.NewGuid().ToString(),
|
||||
Type = TokenEntry.RefreshTokenType,
|
||||
UserId = user.Id.ToString()
|
||||
};
|
||||
var accessToken = new TokenEntry {
|
||||
CreatedAt = DateTime.Now,
|
||||
Token = Guid.NewGuid().ToString(),
|
||||
Type = TokenEntry.AccessTokenType,
|
||||
UserId = user.Id.ToString()
|
||||
};
|
||||
|
||||
context.Tokens.AddRange(refreshToken, accessToken);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Token, new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication<HopDbContextBase>.RefreshTokenTime,
|
||||
HttpOnly = true,
|
||||
Secure = true
|
||||
});
|
||||
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication<TDbContext>.AccessTokenTime,
|
||||
HttpOnly = false,
|
||||
Secure = true
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<bool> Login(UserLogin login) {
|
||||
var user = await userService.GetUserByEmail(login.Email);
|
||||
|
||||
if (user == null) return false;
|
||||
if (await userService.CheckUserPassword(user, login.Password) == false) return false;
|
||||
|
||||
var refreshToken = new TokenEntry {
|
||||
CreatedAt = DateTime.Now,
|
||||
Token = Guid.NewGuid().ToString(),
|
||||
Type = TokenEntry.RefreshTokenType,
|
||||
UserId = user.Id.ToString()
|
||||
};
|
||||
var accessToken = new TokenEntry {
|
||||
CreatedAt = DateTime.Now,
|
||||
Token = Guid.NewGuid().ToString(),
|
||||
Type = TokenEntry.AccessTokenType,
|
||||
UserId = user.Id.ToString()
|
||||
};
|
||||
|
||||
context.Tokens.AddRange(refreshToken, accessToken);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.RefreshTokenType, refreshToken.Token, new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication<HopDbContextBase>.RefreshTokenTime,
|
||||
HttpOnly = true,
|
||||
Secure = true
|
||||
});
|
||||
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication<TDbContext>.AccessTokenTime,
|
||||
HttpOnly = false,
|
||||
Secure = true
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task Logout() {
|
||||
var accessToken = httpAccessor.HttpContext?.Request.Cookies[ITokenContext.AccessTokenType];
|
||||
var refreshToken = httpAccessor.HttpContext?.Request.Cookies[ITokenContext.RefreshTokenType];
|
||||
|
||||
var tokenEntries = await context.Tokens.Where(token =>
|
||||
(token.Token == accessToken && token.Type == TokenEntry.AccessTokenType) ||
|
||||
(token.Token == refreshToken && token.Type == TokenEntry.RefreshTokenType))
|
||||
.ToArrayAsync();
|
||||
|
||||
context.Tokens.Remove(tokenEntries[0]);
|
||||
context.Tokens.Remove(tokenEntries[1]);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
httpAccessor.HttpContext?.Response.Cookies.Delete(ITokenContext.RefreshTokenType);
|
||||
httpAccessor.HttpContext?.Response.Cookies.Delete(ITokenContext.AccessTokenType);
|
||||
}
|
||||
|
||||
public async Task<TokenEntry> RefreshLogin() {
|
||||
var refreshToken = httpAccessor.HttpContext?.Request.Cookies[ITokenContext.RefreshTokenType];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(refreshToken)) return null;
|
||||
|
||||
var token = await context.Tokens.SingleOrDefaultAsync(token => token.Token == refreshToken && token.Type == TokenEntry.RefreshTokenType);
|
||||
|
||||
if (token is null) return null;
|
||||
|
||||
var oldAccessTokens = context.Tokens
|
||||
.AsEnumerable()
|
||||
.Where(old =>
|
||||
old.Type == TokenEntry.AccessTokenType &&
|
||||
old.UserId == token.UserId &&
|
||||
old.CreatedAt + HopFrameAuthentication<TDbContext>.AccessTokenTime < DateTime.Now)
|
||||
.ToList();
|
||||
if (oldAccessTokens.Count != 0)
|
||||
context.Tokens.RemoveRange(oldAccessTokens);
|
||||
|
||||
var oldRefreshTokens = context.Tokens
|
||||
.AsEnumerable()
|
||||
.Where(old =>
|
||||
old.Type == TokenEntry.RefreshTokenType &&
|
||||
old.UserId == token.UserId &&
|
||||
old.CreatedAt + HopFrameAuthentication<TDbContext>.RefreshTokenTime < DateTime.Now)
|
||||
.ToList();
|
||||
if (oldRefreshTokens.Count != 0)
|
||||
context.Tokens.RemoveRange(oldRefreshTokens);
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
if (token.CreatedAt + HopFrameAuthentication<TDbContext>.RefreshTokenTime < DateTime.Now) return null;
|
||||
|
||||
var accessToken = new TokenEntry {
|
||||
CreatedAt = DateTime.Now,
|
||||
Token = Guid.NewGuid().ToString(),
|
||||
Type = TokenEntry.AccessTokenType,
|
||||
UserId = token.UserId
|
||||
};
|
||||
|
||||
await context.Tokens.AddAsync(accessToken);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
httpAccessor.HttpContext?.Response.Cookies.Append(ITokenContext.AccessTokenType, accessToken.Token, new CookieOptions {
|
||||
MaxAge = HopFrameAuthentication<TDbContext>.AccessTokenTime,
|
||||
HttpOnly = false,
|
||||
Secure = true
|
||||
});
|
||||
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public async Task<bool> IsLoggedIn() {
|
||||
var accessToken = httpAccessor.HttpContext?.Request.Cookies[ITokenContext.AccessTokenType];
|
||||
if (string.IsNullOrEmpty(accessToken)) return false;
|
||||
|
||||
var tokenEntry = await context.Tokens.SingleOrDefaultAsync(token => token.Token == accessToken);
|
||||
|
||||
if (tokenEntry is null) return false;
|
||||
if (tokenEntry.CreatedAt + HopFrameAuthentication<TDbContext>.AccessTokenTime < DateTime.Now) return false;
|
||||
if (!await context.Users.AnyAsync(user => user.Id == tokenEntry.UserId)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user