Added permission groups. Cleaned security ui. Added some permission stuff

This commit is contained in:
Marcel Baumgartner
2023-07-17 22:16:39 +02:00
parent 0015001d7c
commit d3b55d155b
10 changed files with 430 additions and 245 deletions

View File

@@ -22,6 +22,10 @@ public class PermissionStorage
{ {
return BitHelper.ReadBit(Data, permission.Index); return BitHelper.ReadBit(Data, permission.Index);
} }
catch (ArgumentOutOfRangeException)
{
return false;
}
catch (Exception e) catch (Exception e)
{ {
Logger.Verbose("Error reading permissions. (Can be intentional)"); Logger.Verbose("Error reading permissions. (Can be intentional)");
@@ -37,4 +41,15 @@ public class PermissionStorage
Data = BitHelper.WriteBit(Data, permission.Index, value); Data = BitHelper.WriteBit(Data, permission.Index, value);
} }
} }
public bool HasAnyPermissions()
{
foreach (var permission in Permissions.GetAllPermissions())
{
if (this[permission])
return true;
}
return false;
}
} }

View File

@@ -261,10 +261,10 @@ public static class Permissions
Description = "Manage mail settings in the admin area" Description = "Manage mail settings in the admin area"
}; };
public static Permission AdminSysMalware = new() public static Permission AdminSecurityMalware = new()
{ {
Index = 39, Index = 39,
Name = "Admin system Malware", Name = "Admin security Malware",
Description = "Manage malware settings in the admin area" Description = "Manage malware settings in the admin area"
}; };
@@ -275,11 +275,11 @@ public static class Permissions
Description = "View system resources in the admin area" Description = "View system resources in the admin area"
}; };
public static Permission AdminSysSecurity = new() public static Permission AdminSecurity = new()
{ {
Index = 41, Index = 41,
Name = "Admin system Security", Name = "Admin Security",
Description = "Manage security settings in the admin area" Description = "View security logs in the admin area"
}; };
public static Permission AdminSysSentry = new() public static Permission AdminSysSentry = new()
@@ -380,6 +380,20 @@ public static class Permissions
Description = "Create a new webspace server in the admin area" Description = "Create a new webspace server in the admin area"
}; };
public static Permission AdminSecurityIpBans = new()
{
Index = 56,
Name = "Admin security ip bans",
Description = "Manage ip bans in the admin area"
};
public static Permission AdminSecurityPermissionGroups = new()
{
Index = 57,
Name = "Admin security permission groups",
Description = "View, add and delete permission groups"
};
public static Permission? FromString(string name) public static Permission? FromString(string name)
{ {
var type = typeof(Permissions); var type = typeof(Permissions);

View File

@@ -243,10 +243,21 @@ public class IdentityService
return; return;
} }
Permissions = new PermissionStorage(BitHelper.OverwriteByteArrays( Permissions = new(Array.Empty<byte>());
UserPermissions.Data,
GroupPermissions.Data), foreach (var permission in Perms.Permissions.GetAllPermissions())
true {
); Permissions[permission] = GroupPermissions[permission];
}
foreach (var permission in Perms.Permissions.GetAllPermissions())
{
if (UserPermissions[permission])
{
Permissions[permission] = true;
}
}
Permissions.IsReadyOnly = true;
} }
} }

View File

@@ -0,0 +1,32 @@
<div class="card mb-5 mb-xl-10">
<div class="card-body pt-0 pb-0">
<ul class="nav nav-stretch nav-line-tabs nav-line-tabs-2x border-transparent fs-5 fw-bold">
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 0 ? "active" : "")" href="/admin/security">
<TL>Overview</TL>
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 1 ? "active" : "")" href="/admin/security/malware">
<TL>Malware</TL>
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 2 ? "active" : "")" href="/admin/security/ipbans">
<TL>Ip bans</TL>
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 3 ? "active" : "")" href="/admin/security/permissiongroups">
<TL>Permission groups</TL>
</a>
</li>
</ul>
</div>
</div>
@code
{
[Parameter]
public int Index { get; set; } = 0;
}

View File

@@ -69,7 +69,7 @@ else
</a> </a>
</div> </div>
if (User.Admin) if (IdentityService.Permissions.HasAnyPermissions())
{ {
<div class="menu-item pt-5"> <div class="menu-item pt-5">
<div class="menu-content"> <div class="menu-content">
@@ -92,6 +92,14 @@ else
<span class="menu-title"><TL>System</TL></span> <span class="menu-title"><TL>System</TL></span>
</a> </a>
</div> </div>
<div class="menu-item">
<a class="menu-link" href="/admin/security">
<span class="menu-icon">
<i class="bx bx-shield"></i>
</span>
<span class="menu-title"><TL>Security</TL></span>
</a>
</div>
<div class="menu-item"> <div class="menu-item">
<a class="menu-link" href="/admin/servers"> <a class="menu-link" href="/admin/servers">
<span class="menu-icon"> <span class="menu-icon">

View File

@@ -0,0 +1,7 @@
@page "/admin/security"
@using Moonlight.Shared.Components.Navigations
@attribute [PermissionRequired(nameof(Permissions.AdminSecurity))]
<AdminSecurityNavigation Index="0" />

View File

@@ -1,4 +1,4 @@
@page "/admin/system/security" @page "/admin/security/ipbans"
@using Moonlight.Shared.Components.Navigations @using Moonlight.Shared.Components.Navigations
@using BlazorTable @using BlazorTable
@@ -13,9 +13,9 @@
@inject EventSystem Event @inject EventSystem Event
@inject ToastService ToastService @inject ToastService ToastService
@attribute [PermissionRequired(nameof(Permissions.AdminSysSecurity))] @attribute [PermissionRequired(nameof(Permissions.AdminSecurityIpBans))]
<AdminSystemNavigation Index="3"/> <AdminSecurityNavigation Index="2"/>
<div class="card mb-5"> <div class="card mb-5">
<div class="card-header"> <div class="card-header">
@@ -41,7 +41,8 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<LazyLoader @ref="LazyLoader" Load="Load"> <LazyLoader @ref="LazyLoader" Load="Load">
<Table TableItem="IpBan" Items="IpBans" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted"> <div class="table-responsive">
<Table TableItem="IpBan" Items="AllIpBans" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
<Column TableItem="IpBan" Title="@(SmartTranslateService.Translate("Ip"))" Field="@(x => x.Ip)" Filterable="true" Sortable="false"/> <Column TableItem="IpBan" Title="@(SmartTranslateService.Translate("Ip"))" Field="@(x => x.Ip)" Filterable="true" Sortable="false"/>
<Column TableItem="IpBan" Title="" Field="@(x => x.Id)" Filterable="false" Sortable="false"> <Column TableItem="IpBan" Title="" Field="@(x => x.Id)" Filterable="false" Sortable="false">
<Template> <Template>
@@ -52,20 +53,21 @@
</Column> </Column>
<Pager ShowPageNumber="true" ShowTotalCount="true"/> <Pager ShowPageNumber="true" ShowTotalCount="true"/>
</Table> </Table>
</div>
</LazyLoader> </LazyLoader>
</div> </div>
</div> </div>
@code @code
{ {
private IpBan[] IpBans; private IpBan[] AllIpBans;
private string Ip; private string Ip;
private LazyLoader LazyLoader; private LazyLoader LazyLoader;
private Task Load(LazyLoader arg) private Task Load(LazyLoader arg)
{ {
IpBans = IpBanRepository.Get().ToArray(); AllIpBans = IpBanRepository.Get().ToArray();
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@@ -1,4 +1,4 @@
@page "/admin/system/malware" @page "/admin/security/malware"
@using Moonlight.Shared.Components.Navigations @using Moonlight.Shared.Components.Navigations
@using Moonlight.App.Services.Background @using Moonlight.App.Services.Background
@@ -14,9 +14,9 @@
@implements IDisposable @implements IDisposable
@attribute [PermissionRequired(nameof(Permissions.AdminSysMalware))] @attribute [PermissionRequired(nameof(Permissions.AdminSecurityMalware))]
<AdminSystemNavigation Index="2"/> <AdminSecurityNavigation Index="1"/>
<div class="row"> <div class="row">
<div class="col-12 col-lg-6"> <div class="col-12 col-lg-6">

View File

@@ -0,0 +1,132 @@
@page "/admin/security/permissiongroups"
@using Moonlight.Shared.Components.Navigations
@using Moonlight.App.Services
@using Moonlight.App.Repositories
@using Moonlight.App.Database.Entities
@using BlazorTable
@using Microsoft.EntityFrameworkCore
@using Moonlight.App.Services.Interop
@using Moonlight.App.Services.Sessions
@inject SmartTranslateService SmartTranslateService
@inject Repository<PermissionGroup> PermissionGroupRepository
@inject SessionServerService SessionServerService
@inject Repository<User> UserRepository
@inject AlertService AlertService
@inject ToastService ToastService
@attribute [PermissionRequired(nameof(Permissions.AdminSecurityPermissionGroups))]
<AdminSecurityNavigation Index="3"/>
<div class="card">
<div class="card-header">
<span class="card-title">
<TL>Permission groups</TL>
</span>
<div class="card-toolbar">
<WButton Text="@(SmartTranslateService.Translate("New"))"
CssClasses="btn-sm btn-success"
OnClick="NewGroupPermission">
</WButton>
</div>
</div>
<div class="card-body">
<LazyLoader @ref="LazyLoader" Load="Load">
<div class="table-responsive">
<Table TableItem="PermissionGroup" Items="AllPermissionGroups" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
<Column TableItem="PermissionGroup" Title="@(SmartTranslateService.Translate("Name"))" Field="@(x => x.Name)" Filterable="true" Sortable="false"/>
<Column TableItem="PermissionGroup" Title="" Field="@(x => x.Id)" Filterable="false" Sortable="false">
<Template>
<div class="text-end">
<WButton Text="@(SmartTranslateService.Translate("Edit permissions"))"
CssClasses="btn-primary me-2"
OnClick="() => EditPermissions(context)">
</WButton>
<DeleteButton Confirm="true" OnClick="() => DeletePermissionGroup(context)"></DeleteButton>
</div>
</Template>
</Column>
<Pager ShowPageNumber="true" ShowTotalCount="true"/>
</Table>
</div>
</LazyLoader>
</div>
</div>
<PermissionEditor @ref="PermissionEditor" OnSave="OnSave"/>
@code
{
private PermissionGroup[] AllPermissionGroups;
private PermissionGroup CurrentPermissionGroup;
private LazyLoader LazyLoader;
private PermissionEditor PermissionEditor;
private Task Load(LazyLoader arg)
{
AllPermissionGroups = PermissionGroupRepository
.Get()
.ToArray();
return Task.CompletedTask;
}
private async Task EditPermissions(PermissionGroup group)
{
CurrentPermissionGroup = group;
PermissionEditor.InitialData = CurrentPermissionGroup.Permissions;
await PermissionEditor.Launch();
}
private async Task DeletePermissionGroup(PermissionGroup group)
{
PermissionGroupRepository.Delete(group);
await LazyLoader.Reload();
}
private async Task OnSave(byte[] data)
{
CurrentPermissionGroup.Permissions = data;
PermissionGroupRepository.Update(CurrentPermissionGroup);
await ToastService.Success("Successfully modified permissions");
var usersWithTheGroup = UserRepository
.Get()
.Include(x => x.PermissionGroup)
.Where(x => x.PermissionGroup != null)
.Where(x => x.PermissionGroup!.Id == CurrentPermissionGroup.Id)
.ToArray();
foreach (var user in usersWithTheGroup)
{
await SessionServerService.ReloadUserSessions(user);
}
}
private async Task NewGroupPermission()
{
var name = await AlertService.Text(
SmartTranslateService.Translate("Enter the name for the new group"),
"",
""
);
if(string.IsNullOrEmpty(name))
return;
var group = new PermissionGroup()
{
Name = name,
Permissions = Array.Empty<byte>()
};
PermissionGroupRepository.Add(group);
await LazyLoader.Reload();
}
}

View File

@@ -1,14 +1,15 @@
@page "/admin/users/view/{Id:int}" @page "/admin/users/view/{Id:int}"
@using Moonlight.App.Database.Entities @using Moonlight.App.Database.Entities
@using Moonlight.App.Helpers
@using Moonlight.App.Repositories @using Moonlight.App.Repositories
@using Moonlight.App.Repositories.Servers
@using Microsoft.EntityFrameworkCore @using Microsoft.EntityFrameworkCore
@using Moonlight.App.Repositories.Domains @using Moonlight.App.Helpers
@using Moonlight.App.Services.Files
@inject UserRepository UserRepository @inject Repository<User> UserRepository
@inject ServerRepository ServerRepository @inject Repository<Server> ServerRepository
@inject DomainRepository DomainRepository @inject Repository<Domain> DomainRepository
@inject Repository<WebSpace> WebSpaceRepository
@inject ResourceService ResourceService
@attribute [PermissionRequired(nameof(Permissions.AdminUserView))] @attribute [PermissionRequired(nameof(Permissions.AdminUserView))]
@@ -21,33 +22,105 @@
} }
else else
{ {
<div class="row"> <div class="d-flex flex-column flex-lg-row">
<div class="col-md-4"> <div class="flex-column flex-lg-row-auto w-lg-250px w-xl-350px mb-10">
<div class="card card-body mb-5"> <div class="card mb-5 mb-xl-8">
<div class="d-flex flex-column align-items-center text-center"> <div class="card-body">
<img src="/api/moonlight/avatar/@(User.Id)" class="rounded-circle" alt="Profile picture" width="150"> <div class="d-flex flex-center flex-column py-5">
<div class="symbol symbol-100px symbol-circle mb-7">
<img src="@(ResourceService.Avatar(User))" alt="Avatar">
</div>
<span class="fs-3 text-gray-800 fw-bold mb-3">
@(User.FirstName) @(User.LastName)
</span>
@if (User.Admin)
{
<div class="mb-5">
<div class="badge badge-lg badge-light-primary d-inline">
<TL>Admin</TL>
</div> </div>
</div> </div>
<div class="card card-body mb-5"> }
<div class="btn-group"> </div>
<a class="btn btn-primary" href="/admin/users/edit/@(User.Id)"> <div>
<div class="pb-5 fs-6">
<div class="fw-bold mt-5">
<TL>Account ID</TL>
</div>
<div class="text-gray-600">@(User.Id)</div>
<div class="fw-bold mt-5">Email</div>
<div class="text-gray-600">
@(User.Email)
</div>
<div class="fw-bold mt-5">
<TL>Address</TL>
</div>
<div class="text-gray-600">@(User.Address), <br>@(User.City)<br>@(User.State)<br>@(User.Country)</div>
<div class="fw-bold mt-5">
<TL>Status</TL>
</div>
<div class="text-gray-600">
@(User.Status)
</div>
<div class="fw-bold mt-5">
<TL>TOTP</TL>
</div>
<div class="text-gray-600">
@(User.TotpEnabled)
</div>
<div class="fw-bold mt-5">
<TL>Discord ID</TL>
</div>
<div class="text-gray-600">
@(User.DiscordId)
</div>
<div class="fw-bold mt-5">
<TL>Last Login IP</TL>
</div>
<div class="text-gray-600">
@(User.LastIp) <TL>at</TL> @(Formatter.FormatDate(User.LastVisitedAt))
</div>
<div class="fw-bold mt-5">
<TL>Register IP</TL>
</div>
<div class="text-gray-600">
@(User.RegisterIp) <TL>at</TL> @(Formatter.FormatDate(User.CreatedAt))
</div>
</div>
</div>
</div>
</div>
</div>
<div class="flex-lg-row-fluid ms-lg-15">
<div class="card mb-3">
<div class="card-header border-0">
<span class="card-title">
<TL>Services</TL>
</span>
<div class="card-toolbar">
<a href="/admin/users/edit/@(User.Id)" class="btn btn-primary">
<TL>Edit</TL> <TL>Edit</TL>
</a> </a>
<a class="btn btn-secondary" href="/admin/users">
<TL>Back to list</TL>
</a>
<a class="btn btn-primary" href="/admin/support/view/@(User.Id)">
<TL>Open support</TL>
</a>
</div> </div>
</div> </div>
<div class="card card-xl-stretch mb-5"> </div>
<div class="card-header border-0">
<h3 class="card-title fw-bold text-dark"> <div class="accordion mb-3" id="serversList">
<div class="accordion-item">
<h2 class="accordion-header" id="serversList-header">
<button class="accordion-button fs-4 fw-semibold collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#serversList-body" aria-expanded="false" aria-controls="serversList-body">
<TL>Servers</TL> <TL>Servers</TL>
</h3> </button>
</div> </h2>
<div class="card-body pt-2"> <div id="serversList-body" class="accordion-collapse collapse" aria-labelledby="serversList-header" data-bs-parent="#serversList">
<div class="accordion-body">
@foreach (var server in Servers) @foreach (var server in Servers)
{ {
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
@@ -63,13 +136,18 @@
} }
</div> </div>
</div> </div>
<div class="card card-xl-stretch">
<div class="card-header border-0">
<h3 class="card-title fw-bold text-dark">
<TL>Domains</TL>
</h3>
</div> </div>
<div class="card-body pt-2"> </div>
<div class="accordion mb-3" id="domainsList">
<div class="accordion-item">
<h2 class="accordion-header" id="domainsList-header">
<button class="accordion-button fs-4 fw-semibold collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#domainsList-body" aria-expanded="false" aria-controls="domainsList-body">
<TL>Domains</TL>
</button>
</h2>
<div id="domainsList-body" class="accordion-collapse collapse" aria-labelledby="domainsList-header" data-bs-parent="#domainsList">
<div class="accordion-body">
@foreach (var domain in Domains) @foreach (var domain in Domains)
{ {
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
@@ -86,152 +164,30 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-8">
<div class="card mb-3">
<div class="card-body fs-6">
<div class="row">
<label class="col-lg-4 fw-semibold text-muted">
<TL>First name</TL>
</label>
<div class="col-lg-8">
<span class="fw-bold fs-6 text-gray-800">@(User.FirstName)</span>
</div> </div>
</div>
<div class="separator my-4"></div>
<div class="row">
<label class="col-lg-4 fw-semibold text-muted">
<TL>Last name</TL>
</label>
<div class="col-lg-8">
<span class="fw-bold fs-6 text-gray-800">@(User.LastName)</span>
</div>
</div>
<div class="separator my-4"></div>
<div class="row">
<label class="col-lg-4 fw-semibold text-muted">
<TL>Email</TL>
</label>
<div class="col-lg-8">
<span class="fw-bold fs-6 text-gray-800">@(User.Email)</span>
</div>
</div>
<div class="separator my-4"></div>
<div class="row">
<label class="col-lg-4 fw-semibold text-muted">
<TL>Address</TL>
</label>
<div class="col-lg-8">
<span class="fw-bold fs-6 text-gray-800">@(User.Address)</span>
</div>
</div>
<div class="separator my-4"></div>
<div class="row">
<label class="col-lg-4 fw-semibold text-muted">
<TL>City</TL>
</label>
<div class="col-lg-8">
<span class="fw-bold fs-6 text-gray-800">@(User.City)</span>
</div>
</div>
<div class="separator my-4"></div>
<div class="row">
<label class="col-lg-4 fw-semibold text-muted">
<TL>State</TL>
</label>
<div class="col-lg-8">
<span class="fw-bold fs-6 text-gray-800">@(User.State)</span>
</div>
</div>
<div class="separator my-4"></div>
<div class="row">
<label class="col-lg-4 fw-semibold text-muted">
<TL>Country</TL>
</label>
<div class="col-lg-8">
<span class="fw-bold fs-6 text-gray-800">@(User.Country)</span>
</div>
</div>
<div class="separator my-4"></div>
<div class="row">
<label class="col-lg-4 fw-semibold text-muted">
<TL>Admin</TL>
</label>
<div class="col-lg-8">
<span class="fw-bold fs-6 text-gray-800">
@if (User.Admin)
{
<span>✅</span>
}
else
{
<span>❌</span>
}
</span>
</div>
</div>
<div class="separator my-4"></div>
<div class="row">
<label class="col-lg-4 fw-semibold text-muted">
<TL>Status</TL>
</label>
<div class="col-lg-8">
<span class="fw-bold fs-6 text-gray-800">@(User.Status)</span>
</div>
</div>
<div class="separator my-4"></div>
<div class="row">
<label class="col-lg-4 fw-semibold text-muted">
<TL>Totp</TL>
</label>
<div class="col-lg-8">
<span class="fw-bold fs-6 text-gray-800">@(User.TotpEnabled)</span>
</div>
</div>
<div class="separator my-4"></div>
<div class="row">
<label class="col-lg-4 fw-semibold text-muted">
<TL>Discord</TL>
</label>
<div class="col-lg-8">
<span class="fw-bold fs-6 text-gray-800">@(User.DiscordId)</span>
</div>
</div>
<div class="separator my-4"></div>
<div class="row">
<label class="col-lg-4 fw-semibold text-muted">
<TL>Subscription</TL>
</label>
<div class="col-lg-8">
<span class="fw-bold fs-6 text-gray-800">
</span> <div class="accordion mb-3" id="webspacesList">
<div class="accordion-item">
<h2 class="accordion-header" id="webspacesList-header">
<button class="accordion-button fs-4 fw-semibold collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#webspacesList-body" aria-expanded="false" aria-controls="webspacesList-body">
<TL>Webspaces</TL>
</button>
</h2>
<div id="webspacesList-body" class="accordion-collapse collapse" aria-labelledby="webspacesList-header" data-bs-parent="#webspacesList">
<div class="accordion-body">
@foreach (var webSpace in WebSpaces)
{
<div class="d-flex align-items-center">
<div class="flex-grow-1">
<a href="/webspace/@(webSpace.Id)" class="fs-6">@(webSpace.Domain) - @(webSpace.CloudPanel.Name)</a>
</div> </div>
</div> </div>
if (webSpace != WebSpaces.Last())
{
<div class="separator my-4"></div> <div class="separator my-4"></div>
<div class="row"> }
<label class="col-lg-4 fw-semibold text-muted"> }
<TL>Created at</TL>
</label>
<div class="col-lg-8">
<span class="fw-bold fs-6 text-gray-800">@(Formatter.FormatDate(User.CreatedAt))</span>
</div>
</div>
<div class="separator my-4"></div>
<div class="row">
<label class="col-lg-4 fw-semibold text-muted">
<TL>Register ip</TL>
</label>
<div class="col-lg-8">
<span class="fw-bold fs-6 text-gray-800">@(User.RegisterIp)</span>
</div>
</div>
<div class="separator my-4"></div>
<div class="row">
<label class="col-lg-4 fw-semibold text-muted">
<TL>Last ip</TL>
</label>
<div class="col-lg-8">
<span class="fw-bold fs-6 text-gray-800">@(User.LastIp)</span>
</div> </div>
</div> </div>
</div> </div>
@@ -239,7 +195,7 @@
</div> </div>
</div> </div>
} }
</LazyLoader> </LazyLoader>
@code @code
{ {
@@ -249,6 +205,7 @@
private User? User; private User? User;
private Server[] Servers; private Server[] Servers;
private Domain[] Domains; private Domain[] Domains;
private WebSpace[] WebSpaces;
private Task Load(LazyLoader arg) private Task Load(LazyLoader arg)
{ {
@@ -269,6 +226,13 @@
.Include(x => x.Owner) .Include(x => x.Owner)
.Where(x => x.Owner.Id == User.Id) .Where(x => x.Owner.Id == User.Id)
.ToArray(); .ToArray();
WebSpaces = WebSpaceRepository
.Get()
.Include(x => x.CloudPanel)
.Include(x => x.Owner)
.Where(x => x.Owner.Id == User.Id)
.ToArray();
} }
return Task.CompletedTask; return Task.CompletedTask;