Reorganized folder structure

This commit is contained in:
2024-09-26 10:20:30 +02:00
parent af7385678f
commit 27088f8217
92 changed files with 16 additions and 31 deletions

View 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

View 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);
}
}

View 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;
}

View 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
}
}

View 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;
}

View File

@@ -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>

View 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);
}
}

View File

@@ -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>

View 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
}
}

View 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;
}