Skip to content

Commit

Permalink
Add RoleStore and roles UI.
Browse files Browse the repository at this point in the history
  • Loading branch information
bitbound committed Nov 13, 2024
1 parent 20e6a3b commit 57bf023
Show file tree
Hide file tree
Showing 33 changed files with 855 additions and 1,008 deletions.
6 changes: 3 additions & 3 deletions ControlR.Web.Client/Authz/RoleNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

public static class RoleNames
{
public const string ServerAdministrator = "ServerAdministrator";
public const string TenantAdministrator = "TenantAdministrator";
public const string DeviceSuperUser = "DeviceSuperUser";
public const string ServerAdministrator = "Server Administrator";
public const string TenantAdministrator = "Tenant Administrator";
public const string DeviceSuperUser = "Device Superuser";
}
1 change: 1 addition & 0 deletions ControlR.Web.Client/Components/Pages/Permissions.razor
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<TagsTabContent />
</MudTabPanel>
<MudTabPanel Text="Users" Icon="@(Icons.Material.Filled.AccountCircle)" IconColor="Color.Secondary">
<UsersTabContent />
</MudTabPanel>
<MudTabPanel Text="Devices" Icon="@(Icons.Material.Filled.Devices)" IconColor="Color.Secondary">
</MudTabPanel>
Expand Down
4 changes: 4 additions & 0 deletions ControlR.Web.Client/Components/Pages/Permissions.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ public partial class Permissions : ComponentBase
[Inject]
public required IUserStore UserStore { get; init; }

[Inject]
public required IRoleStore RoleStore { get; init; }

protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
Expand All @@ -37,5 +40,6 @@ private async Task Refresh()
await DeviceStore.Refresh();
await TagStore.Refresh();
await UserStore.Refresh();
await RoleStore.Refresh();
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@using ControlR.Web.Client.ViewModels
<MudGrid Justify="Justify.Center">
<MudGrid>
<MudItem md="4" sm="6" xs="12">
<MudText Typo="Typo.h6" Class="mt-2 mb-4 text-center">
Tags
Expand Down
86 changes: 86 additions & 0 deletions ControlR.Web.Client/Components/Permissions/UsersTabContent.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<MudGrid>
<MudItem md="4" sm="6" xs="12">
<MudText Typo="Typo.h6" Class="mt-2 mb-4 text-center">
Users
</MudText>

<MudTextField T="string"
@bind-Value="_userSearchPattern"
Label="Search"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search"
Immediate="true"
Class="my-2"
Clearable="true" />

<MudPaper MaxHeight="500px" Class="overflow-y-auto">
<MudList T="UserResponseDto"
@bind-SelectedValue="_selectedUser"
SelectionMode="SelectionMode.SingleSelection"
Color="Color.Info">

@foreach (var user in FilteredUsers)
{
<MudListItem T="UserResponseDto" Value="@user">
@user.UserName
</MudListItem>
}

</MudList>
</MudPaper>
</MudItem>
<MudItem md="4" sm="6" xs="12">
<MudText Typo="Typo.h6" Class="mt-2 mb-4 text-center">
Roles
</MudText>
@if (_selectedUser is not null)
{
<MudTextField T="string"
@bind-Value="_roleSearchPattern"
Label="Search"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search"
Immediate="true"
Class="my-2"
Clearable="true" />
<MudPaper MaxHeight="500px" Class="pa-3 overflow-y-auto">
@foreach (var role in FilteredRoles)
{
<MudSwitch T="bool"
Value="@(role.UserIds.Contains(_selectedUser.Id))"
Color="Color.Success"
ValueChanged="@(async isToggled => await SetUserRole(isToggled, _selectedUser, role))">
@role.Name
</MudSwitch>
}
</MudPaper>
}
</MudItem>
<MudItem md="4" sm="6" xs="12">
<MudText Typo="Typo.h6" Class="mt-2 mb-4 text-center">
Tags
</MudText>
@if (_selectedUser is not null)
{
<MudTextField T="string"
@bind-Value="_tagSearchPattern"
Label="Search"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search"
Immediate="true"
Class="my-2"
Clearable="true" />
<MudPaper MaxHeight="500px" Class="pa-3 overflow-y-auto">
@foreach (var tag in FilteredTags)
{
<MudSwitch T="bool"
Value="@(tag.UserIds.Contains(_selectedUser.Id))"
Color="Color.Success"
ValueChanged="@(async isToggled => await SetUserTag(isToggled, _selectedUser, tag))">
@tag.Name
</MudSwitch>
}
</MudPaper>
}
</MudItem>
</MudGrid>
166 changes: 166 additions & 0 deletions ControlR.Web.Client/Components/Permissions/UsersTabContent.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
using ControlR.Web.Client.Extensions;
using ControlR.Web.Client.Services.Stores;
using ControlR.Web.Client.ViewModels;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using System.Collections.Immutable;

namespace ControlR.Web.Client.Components.Permissions;

public partial class UsersTabContent : ComponentBase, IDisposable
{
private string _tagSearchPattern = string.Empty;
private string _userSearchPattern = string.Empty;
private string _roleSearchPattern = string.Empty;
private UserResponseDto? _selectedUser;
private bool _isServerAdmin;
private Guid _currentUserId;

private ImmutableArray<IDisposable>? _changeHandlers;

[Inject]
public required AuthenticationStateProvider AuthState { get; init; }
[Inject]
public required IControlrApi ControlrApi { get; init; }

[Inject]
public required ILogger<TagsTabContent> Logger { get; init; }

[Inject]
public required IRoleStore RoleStore { get; init; }

[Inject]
public required ISnackbar Snackbar { get; init; }

[Inject]
public required ITagStore TagStore { get; init; }

[Inject]
public required IUserStore UserStore { get; init; }

private IOrderedEnumerable<RoleViewModel> FilteredRoles
{
get
{
var query = RoleStore.Items
.Where(x => x.Name.Contains(_roleSearchPattern, StringComparison.OrdinalIgnoreCase));

if (!_isServerAdmin)
{
query = query.Where(x => x.Name != RoleNames.ServerAdministrator);
}

if (_selectedUser?.Id == _currentUserId)
{
query = query.Where(x =>
x.Name is not RoleNames.ServerAdministrator and not RoleNames.TenantAdministrator);
}

return query.OrderBy(x => x.Name);
}
}

private IOrderedEnumerable<TagViewModel> FilteredTags =>
TagStore.Items
.Where(x => x.Name.Contains(_tagSearchPattern, StringComparison.OrdinalIgnoreCase))
.OrderBy(x => x.Name);

private IOrderedEnumerable<UserResponseDto> FilteredUsers =>
UserStore.Items
.Where(x => x.UserName?.Contains(_userSearchPattern, StringComparison.OrdinalIgnoreCase) == true)
.OrderBy(x => x.UserName);


public void Dispose()
{
_changeHandlers?.DisposeAll();
GC.SuppressFinalize(this);
}

protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
var state = await AuthState.GetAuthenticationStateAsync();
if (state.User.TryGetUserId(out var userId))
{
_currentUserId = userId;
}
_isServerAdmin = state.User.IsInRole(RoleNames.ServerAdministrator);
}

private async Task SetUserTag(bool isToggled, UserResponseDto user, TagViewModel tag)
{
try
{
if (isToggled)
{
var addResult = await ControlrApi.AddUserTag(user.Id, tag.Id);
if (!addResult.IsSuccess)
{
Snackbar.Add(addResult.Reason, Severity.Error);
return;
}
tag.UserIds.Add(user.Id);
}
else
{
var removeResult = await ControlrApi.RemoveUserTag(user.Id, tag.Id);
if (!removeResult.IsSuccess)
{
Snackbar.Add(removeResult.Reason, Severity.Error);
return;
}
tag.UserIds.Remove(user.Id);
}

await TagStore.InvokeItemsChanged();

Snackbar.Add(isToggled
? "Tag added"
: "Tag removed", Severity.Success);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error while setting tag.");
Snackbar.Add("An error occurred while setting tag", Severity.Error);
}
}

private async Task SetUserRole(bool isToggled, UserResponseDto user, RoleViewModel role)
{
try
{
if (isToggled)
{
var addResult = await ControlrApi.AddUserRole(user.Id, role.Id);
if (!addResult.IsSuccess)
{
Snackbar.Add(addResult.Reason, Severity.Error);
return;
}
role.UserIds.Add(user.Id);
}
else
{
var removeResult = await ControlrApi.RemoveUserRole(user.Id, role.Id);
if (!removeResult.IsSuccess)
{
Snackbar.Add(removeResult.Reason, Severity.Error);
return;
}
role.UserIds.Remove(user.Id);
}

await TagStore.InvokeItemsChanged();

Snackbar.Add(isToggled
? "Role added"
: "Role removed", Severity.Success);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error while setting role.");
Snackbar.Add("An error occurred while setting role", Severity.Error);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public static IServiceCollection AddControlrWebClient(this IServiceCollection se
services.AddScoped<IDeviceStore, DeviceStore>();
services.AddScoped<ITagStore, TagStore>();
services.AddScoped<IUserStore, UserStore>();
services.AddScoped<IRoleStore, RoleStore>();

services.AddStronglyTypedSignalrClient<IViewerHub, IViewerHubClient, ViewerHubClient>(ServiceLifetime.Scoped);

Expand Down
34 changes: 34 additions & 0 deletions ControlR.Web.Client/Services/Stores/RoleStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using ControlR.Web.Client.ViewModels;

namespace ControlR.Web.Client.Services.Stores;

public interface IRoleStore : IStoreBase<RoleViewModel>
{ }

internal class RoleStore : StoreBase<RoleViewModel>, IRoleStore
{
public RoleStore(
IControlrApi controlrApi,
ISnackbar snackbar,
ILogger<RoleStore> logger)
: base(controlrApi, snackbar, logger)
{

}

protected override async Task RefreshImpl()
{
var getResult = await ControlrApi.GetAllRoles();
if (!getResult.IsSuccess)
{
Snackbar.Add(getResult.Reason, Severity.Error);
return;
}

foreach (var role in getResult.Value)
{
var vm = new RoleViewModel(role);
Cache.AddOrUpdate(vm.Id, vm, (_, _) => vm);
}
}
}
8 changes: 8 additions & 0 deletions ControlR.Web.Client/ViewModels/RoleViewModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace ControlR.Web.Client.ViewModels;

public class RoleViewModel(RoleResponseDto dto) : IHasPrimaryKey
{
public Guid Id { get; } = dto.Id;
public string Name { get; } = dto.Name;
public ConcurrentHashSet<Guid> UserIds { get; } = new(dto.UserIds);
}
3 changes: 2 additions & 1 deletion ControlR.Web.Client/_Imports.razor
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@
@using System.Runtime.InteropServices
@using System.Text
@using System.Runtime.InteropServices.JavaScript
@using ControlR.Web.Client.Components.Permissions
@using ControlR.Web.Client.Components.Permissions
@using ControlR.Web.Client.ViewModels
7 changes: 1 addition & 6 deletions ControlR.Web.Server/Api/DevicesController.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OutputCaching;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Mvc;

namespace ControlR.Web.Server.Api;

[Route("api/[controller]")]
[ApiController]
[OutputCache(Duration = 60)]
[Authorize]
public class DevicesController : ControllerBase
{
Expand Down
Loading

0 comments on commit 57bf023

Please sign in to comment.