mirror of
https://github.com/immense/Remotely.git
synced 2025-10-26 11:27:15 +00:00
Create authorization policies for OrganizationAdmin and ServerAdmin. Use these within the routing system via Authorize attribute instead of directly checking on each page.
This commit is contained in:
parent
5383343e0a
commit
bc690e8beb
@ -1,7 +1,13 @@
|
||||
<CascadingAuthenticationState>
|
||||
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
|
||||
<Found Context="routeData">
|
||||
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
|
||||
<NotAuthorized>
|
||||
<div class="jumbotron">
|
||||
<h3>You are not authorized to access this resource.</h3>
|
||||
</div>
|
||||
</NotAuthorized>
|
||||
</AuthorizeRouteView>
|
||||
</Found>
|
||||
<NotFound>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
|
||||
8
Server/Auth/OrganizationAdminRequirement.cs
Normal file
8
Server/Auth/OrganizationAdminRequirement.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Remotely.Server.Auth;
|
||||
|
||||
public class OrganizationAdminRequirement : IAuthorizationRequirement
|
||||
{
|
||||
|
||||
}
|
||||
34
Server/Auth/OrganizationAdminRequirementHandler.cs
Normal file
34
Server/Auth/OrganizationAdminRequirementHandler.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Remotely.Shared.Models;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Remotely.Server.Auth;
|
||||
|
||||
public class OrganizationAdminRequirementHandler : AuthorizationHandler<OrganizationAdminRequirement>
|
||||
{
|
||||
private readonly UserManager<RemotelyUser> _userManager;
|
||||
|
||||
public OrganizationAdminRequirementHandler(UserManager<RemotelyUser> userManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, OrganizationAdminRequirement requirement)
|
||||
{
|
||||
if (context.User.Identity?.IsAuthenticated != true)
|
||||
{
|
||||
context.Fail();
|
||||
return;
|
||||
}
|
||||
|
||||
var user = await _userManager.GetUserAsync(context.User);
|
||||
if (user?.IsAdministrator != true)
|
||||
{
|
||||
context.Fail();
|
||||
return;
|
||||
}
|
||||
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
8
Server/Auth/PolicyNames.cs
Normal file
8
Server/Auth/PolicyNames.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Remotely.Server.Auth;
|
||||
|
||||
public static class PolicyNames
|
||||
{
|
||||
public const string TwoFactorRequired = nameof(TwoFactorRequired);
|
||||
public const string OrganizationAdminRequired = nameof(OrganizationAdminRequired);
|
||||
public const string ServerAdminRequired = nameof(ServerAdminRequired);
|
||||
}
|
||||
7
Server/Auth/ServerAdminRequirement.cs
Normal file
7
Server/Auth/ServerAdminRequirement.cs
Normal file
@ -0,0 +1,7 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Remotely.Server.Auth;
|
||||
|
||||
public class ServerAdminRequirement : IAuthorizationRequirement
|
||||
{
|
||||
}
|
||||
36
Server/Auth/ServerAdminRequirementHandler.cs
Normal file
36
Server/Auth/ServerAdminRequirementHandler.cs
Normal file
@ -0,0 +1,36 @@
|
||||
#nullable enable
|
||||
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Remotely.Shared.Models;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Remotely.Server.Auth;
|
||||
|
||||
public class ServerAdminRequirementHandler : AuthorizationHandler<ServerAdminRequirement>
|
||||
{
|
||||
private readonly UserManager<RemotelyUser> _userManager;
|
||||
|
||||
public ServerAdminRequirementHandler(UserManager<RemotelyUser> userManager)
|
||||
{
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ServerAdminRequirement requirement)
|
||||
{
|
||||
if (context.User.Identity?.IsAuthenticated != true)
|
||||
{
|
||||
context.Fail();
|
||||
return;
|
||||
}
|
||||
|
||||
var user = await _userManager.GetUserAsync(context.User);
|
||||
if (user?.IsServerAdmin != true)
|
||||
{
|
||||
context.Fail();
|
||||
return;
|
||||
}
|
||||
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,6 @@ namespace Remotely.Server.Auth
|
||||
{
|
||||
public class TwoFactorRequiredRequirement : IAuthorizationRequirement
|
||||
{
|
||||
public const string PolicyName = "TwoFactorRequired";
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,13 +11,10 @@
|
||||
private bool _loaderShown;
|
||||
private string _statusMessage = string.Empty;
|
||||
|
||||
protected override Task OnAfterRenderAsync(bool firstRender)
|
||||
protected override Task OnInitializedAsync()
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
Messenger.Register<ShowLoaderMessage>(this, HandleShowLoaderMessage);
|
||||
}
|
||||
return base.OnAfterRenderAsync(firstRender);
|
||||
Messenger.Register<ShowLoaderMessage>(this, HandleShowLoaderMessage);
|
||||
return base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
private async Task HandleShowLoaderMessage(ShowLoaderMessage message)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
@page "/api-keys"
|
||||
@attribute [Authorize]
|
||||
@attribute [Authorize(Policy = PolicyNames.OrganizationAdminRequired)]
|
||||
@inherits AuthComponentBase
|
||||
|
||||
@inject IDataService DataService
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
@page "/branding"
|
||||
@inherits AuthComponentBase
|
||||
@attribute [Authorize(Policy = PolicyNames.OrganizationAdminRequired)]
|
||||
@inject IDataService DataService
|
||||
@inject IToastService ToastService
|
||||
@inject IJsInterop JsInterop
|
||||
@ -8,70 +9,61 @@
|
||||
|
||||
<h3 class="mb-3">Branding</h3>
|
||||
|
||||
<AlertBanner Message="@_alertMessage" />
|
||||
|
||||
@if (User?.IsAdministrator != true)
|
||||
{
|
||||
<h5 class="text-muted">Only organization administrators can view this page.</h5>
|
||||
}
|
||||
else
|
||||
{
|
||||
<AlertBanner Message="@_alertMessage" />
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-lg-6 col-xl-5">
|
||||
<div class="form-group">
|
||||
<label class="mb-1">Branding Areas</label>
|
||||
<br />
|
||||
<img class="img-fluid" src="/images/Remotely_Desktop.png" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-lg-6 col-xl-5">
|
||||
<div class="form-group">
|
||||
<label class="mb-1">Branding Areas</label>
|
||||
<br />
|
||||
<img class="img-fluid" src="/images/Remotely_Desktop.png" />
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-lg-6 col-xl-5">
|
||||
<EditForm OnValidSubmit="HandleValidSubmit" Model="_inputModel">
|
||||
<DataAnnotationsValidator />
|
||||
|
||||
<div class="form-group mb-5">
|
||||
<label class="text-info font-weight-bold">Product Name</label>
|
||||
<InputText class="form-control" @bind-Value="_inputModel.ProductName" />
|
||||
<ValidationMessage For="() => _inputModel.ProductName" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(_base64Icon))
|
||||
{
|
||||
<div class="form-group mb-4">
|
||||
<label class="text-info font-weight-bold">Current Icon</label>
|
||||
<br />
|
||||
<img class="img-fluid" src="data:image/png;base64,@_base64Icon" />
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="form-group mb-5">
|
||||
<label class="text-info font-weight-bold">New Icon</label>
|
||||
<InputFile type="file" accept="image/png" class="form-control" OnChange="IconUploadInputChanged" />
|
||||
</div>
|
||||
<div class="form-group mb-5">
|
||||
<label class="text-info font-weight-bold">Title Foreground</label>
|
||||
<ColorPicker @bind-Color="_inputModel.TitleForegroundColor" />
|
||||
</div>
|
||||
<div class="form-group mb-5">
|
||||
<label class="text-info font-weight-bold">Title Background</label>
|
||||
<ColorPicker @bind-Color="_inputModel.TitleBackgroundColor" />
|
||||
</div>
|
||||
<div class="form-group mb-5">
|
||||
<label class="text-info font-weight-bold">Button Color</label>
|
||||
<ColorPicker @bind-Color="_inputModel.TitleButtonColor" />
|
||||
</div>
|
||||
<div class="form-group text-right">
|
||||
<button class="btn btn-secondary mr-3" type="button" @onclick="ResetBranding">Reset</button>
|
||||
<button class="btn btn-primary" type="submit">Submit</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-8 col-lg-6 col-xl-5">
|
||||
<EditForm OnValidSubmit="HandleValidSubmit" Model="_inputModel">
|
||||
<DataAnnotationsValidator />
|
||||
|
||||
<div class="form-group mb-5">
|
||||
<label class="text-info font-weight-bold">Product Name</label>
|
||||
<InputText class="form-control" @bind-Value="_inputModel.ProductName" />
|
||||
<ValidationMessage For="() => _inputModel.ProductName" />
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(_base64Icon))
|
||||
{
|
||||
<div class="form-group mb-4">
|
||||
<label class="text-info font-weight-bold">Current Icon</label>
|
||||
<br />
|
||||
<img class="img-fluid" src="data:image/png;base64,@_base64Icon" />
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="form-group mb-5">
|
||||
<label class="text-info font-weight-bold">New Icon</label>
|
||||
<InputFile type="file" accept="image/png" class="form-control" OnChange="IconUploadInputChanged" />
|
||||
</div>
|
||||
<div class="form-group mb-5">
|
||||
<label class="text-info font-weight-bold">Title Foreground</label>
|
||||
<ColorPicker @bind-Color="_inputModel.TitleForegroundColor" />
|
||||
</div>
|
||||
<div class="form-group mb-5">
|
||||
<label class="text-info font-weight-bold">Title Background</label>
|
||||
<ColorPicker @bind-Color="_inputModel.TitleBackgroundColor" />
|
||||
</div>
|
||||
<div class="form-group mb-5">
|
||||
<label class="text-info font-weight-bold">Button Color</label>
|
||||
<ColorPicker @bind-Color="_inputModel.TitleButtonColor" />
|
||||
</div>
|
||||
<div class="form-group text-right">
|
||||
<button class="btn btn-secondary mr-3" type="button" @onclick="ResetBranding">Reset</button>
|
||||
<button class="btn btn-primary" type="submit">Submit</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
@code {
|
||||
private string _alertMessage;
|
||||
private string _base64Icon;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
@page "/manage-organization"
|
||||
@attribute [Authorize]
|
||||
@attribute [Authorize(Policy = PolicyNames.OrganizationAdminRequired)]
|
||||
@inherits AuthComponentBase
|
||||
|
||||
<h3 class="mb-3">Manage Organization</h3>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
@page "/scripts/{activeTab?}"
|
||||
@attribute [Authorize]
|
||||
@inherits AuthComponentBase
|
||||
@using System.Collections
|
||||
@inject IDataService DataService
|
||||
|
||||
@ -1,395 +1,387 @@
|
||||
@page "/server-config"
|
||||
|
||||
@attribute [Authorize]
|
||||
@attribute [Authorize(Policy = PolicyNames.ServerAdminRequired)]
|
||||
@inherits AuthComponentBase
|
||||
|
||||
|
||||
@if (User?.IsServerAdmin == true)
|
||||
{
|
||||
<AlertBanner Message="@_alertMessage" />
|
||||
<AlertBanner Message="@_alertMessage" />
|
||||
|
||||
<h3 class="mt-3">Server Info</h3>
|
||||
<h3 class="mt-3">Server Info</h3>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="mb-4" style="display:flex; justify-content: space-evenly">
|
||||
<div class="d-inline-block text-center mr-2">
|
||||
<label>Devices Online</label>
|
||||
<br />
|
||||
<span class="badge badge-primary p-2">@ServiceSessionCache.GetAllDevices().Count</span>
|
||||
</div>
|
||||
|
||||
<div class="d-inline-block text-center mr-2">
|
||||
<label>Devices Outdated</label>
|
||||
<br />
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="mb-4" style="display:flex; justify-content: space-evenly">
|
||||
<div class="d-inline-block text-center mr-2">
|
||||
<label>Devices Online</label>
|
||||
<br />
|
||||
<span class="badge badge-primary p-2">@ServiceSessionCache.GetAllDevices().Count</span>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-sm btn-secondary" @onclick="ShowOutdatedDevices">
|
||||
<i class="oi oi-question-mark"></i>
|
||||
</button>
|
||||
<span class="badge badge-primary p-2 mx-2">@(OutdatedDevices?.Count() ?? 0)</span>
|
||||
<button class="btn btn-secondary btn-sm" type="button" title="Update All" @onclick="UpdateAllDevices">
|
||||
<span class="oi oi-reload"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="d-inline-block text-center mr-2">
|
||||
<label>Devices Outdated</label>
|
||||
<br />
|
||||
|
||||
<div class="d-inline-block text-center mr-2">
|
||||
<label>Devices Total</label>
|
||||
<br />
|
||||
<span class="badge badge-primary p-2">@TotalDevices</span>
|
||||
</div>
|
||||
<div class="d-inline-block text-center mr-2">
|
||||
<label>Active Users</label>
|
||||
<br />
|
||||
<span class="badge badge-primary p-2">@CircuitManager.Connections.Count</span>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-secondary" @onclick="ShowOutdatedDevices">
|
||||
<i class="oi oi-question-mark"></i>
|
||||
</button>
|
||||
<span class="badge badge-primary p-2 mx-2">@(OutdatedDevices?.Count() ?? 0)</span>
|
||||
<button class="btn btn-secondary btn-sm" type="button" title="Update All" @onclick="UpdateAllDevices">
|
||||
<span class="oi oi-reload"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="d-inline-block text-center mr-2">
|
||||
<label>Devices Total</label>
|
||||
<br />
|
||||
<span class="badge badge-primary p-2">@TotalDevices</span>
|
||||
</div>
|
||||
<div class="d-inline-block text-center mr-2">
|
||||
<label>Active Users</label>
|
||||
<br />
|
||||
<span class="badge badge-primary p-2">@CircuitManager.Connections.Count</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h3 class="mt3">Server Admins</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">Server Admins</label>
|
||||
<br />
|
||||
|
||||
<div class="small">
|
||||
<input class="align-middle mr-1" type="checkbox" @bind="_showMyOrgAdminsOnly" />
|
||||
<span class="align-middle">Show my organization only</span>
|
||||
</div>
|
||||
<div class="small">
|
||||
<input class="align-middle mr-1" type="checkbox" @bind="_showAdminsOnly" />
|
||||
<span class="align-middle">Show current admins only</span>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<div class="list-box">
|
||||
@foreach (var user in UserList)
|
||||
{
|
||||
<div @key="user.Id">
|
||||
<input type="checkbox" disabled="@(user.Id == User.Id)" checked="@user.IsServerAdmin" @onchange="ev => SetIsServerAdmin(ev, user)" />
|
||||
<span class="ml-2 align-top" style="line-height:1.3em">@user.UserName</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<EditForm Model="Input" OnValidSubmit="HandleValidSubmit" autocomplete="off">
|
||||
<DataAnnotationsValidator />
|
||||
|
||||
<h3 class="mt-3">Application Settings</h3>
|
||||
|
||||
<ValidationSummary Model="Input" />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h3 class="mt3">Server Admins</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">Server Admins</label>
|
||||
<label>Allow API Login</label>
|
||||
<br />
|
||||
|
||||
<div class="small">
|
||||
<input class="align-middle mr-1" type="checkbox" @bind="_showMyOrgAdminsOnly" />
|
||||
<span class="align-middle">Show my organization only</span>
|
||||
</div>
|
||||
<div class="small">
|
||||
<input class="align-middle mr-1" type="checkbox" @bind="_showAdminsOnly" />
|
||||
<span class="align-middle">Show current admins only</span>
|
||||
</div>
|
||||
|
||||
<InputCheckbox @bind-Value="Input.AllowApiLogin" autocomplete="off" />
|
||||
<br />
|
||||
<div class="list-box">
|
||||
@foreach (var user in UserList)
|
||||
{
|
||||
<div @key="user.Id">
|
||||
<input type="checkbox" disabled="@(user.Id == User.Id)" checked="@user.IsServerAdmin" @onchange="ev => SetIsServerAdmin(ev, user)" />
|
||||
<span class="ml-2 align-top" style="line-height:1.3em">@user.UserName</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<ValidationMessage For="() => Input.AllowApiLogin" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Banned Devices</label>
|
||||
<br />
|
||||
<select size="4" class="form-control" @bind="_bannedDeviceSelected">
|
||||
@foreach (var bannedDevice in Input.BannedDevices)
|
||||
{
|
||||
<option @key="bannedDevice" value="@bannedDevice">@bannedDevice</option>
|
||||
}
|
||||
</select>
|
||||
|
||||
<div class="text-right mb-2 mt-1">
|
||||
<button type="button" class="btn btn-secondary" @onclick="RemoveBannedDevice">Remove</button>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input placeholder="Add banned device ID, name, or IP"
|
||||
class="form-control" autocomplete="off"
|
||||
@bind="_bannedDeviceToAdd"
|
||||
@bind:event="oninput"
|
||||
@onkeydown="HandleBannedDeviceKeyDown" />
|
||||
<button type="button" class="btn btn-secondary" @onclick="AddBannedDevice">Add</button>
|
||||
</div>
|
||||
<ValidationMessage For="() => Input.BannedDevices" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Data Retention in Days</label>
|
||||
<br />
|
||||
<InputNumber @bind-Value="Input.DataRetentionInDays" class="form-control" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.DataRetentionInDays" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Database Provider</label>
|
||||
<br />
|
||||
<select class="form-control" @bind="Input.DBProvider">
|
||||
@foreach (var provider in Enum.GetValues<DbProvider>())
|
||||
{
|
||||
<option @key="provider" value="@provider">@provider</option>
|
||||
}
|
||||
</select>
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.DBProvider" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Enable Windows Event Log</label>
|
||||
<br />
|
||||
<InputCheckbox @bind-Value="Input.EnableWindowsEventLog" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.EnableWindowsEventLog" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Enforce Attended Access</label>
|
||||
<br />
|
||||
<InputCheckbox @bind-Value="Input.EnforceAttendedAccess" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.EnforceAttendedAccess" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Force Client HTTPS</label>
|
||||
<br />
|
||||
<InputCheckbox @bind-Value="Input.ForceClientHttps" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.ForceClientHttps" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Known Proxies</label>
|
||||
<br />
|
||||
<select class="form-control" @bind="_knownProxySelected" size="4">
|
||||
@foreach (var proxy in Input.KnownProxies)
|
||||
{
|
||||
<option @key="proxy" value="@proxy">@proxy</option>
|
||||
}
|
||||
</select>
|
||||
<div class="text-right mb-2 mt-1">
|
||||
<button type="button" class="btn btn-secondary" @onclick="RemoveKnownProxy">Remove</button>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input @bind="_knownProxyToAdd"
|
||||
@bind:event="oninput"
|
||||
placeholder="Add a known proxy"
|
||||
class="form-control"
|
||||
autocomplete="off"
|
||||
@onkeydown="HandleKnownProxyKeyDown" />
|
||||
|
||||
<button type="button" class="btn btn-secondary" @onclick="AddKnownProxy">Add</button>
|
||||
</div>
|
||||
<ValidationMessage For="() => Input.KnownProxies" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Max Organization Count</label>
|
||||
<br />
|
||||
<InputNumber @bind-Value="Input.MaxOrganizationCount" class="form-control" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.MaxOrganizationCount" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Max Concurrent Updates</label>
|
||||
<br />
|
||||
<InputNumber @bind-Value="Input.MaxConcurrentUpdates" class="form-control" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.MaxConcurrentUpdates" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Message of the Day</label>
|
||||
<br />
|
||||
<InputText @bind-Value="Input.MessageOfTheDay" class="form-control" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.MessageOfTheDay" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Redirect to HTTPS</label>
|
||||
<br />
|
||||
<InputCheckbox @bind-Value="Input.RedirectToHttps" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.RedirectToHttps" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Remote Control Notify User</label>
|
||||
<br />
|
||||
<InputCheckbox @bind-Value="Input.RemoteControlNotifyUser" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.RemoteControlNotifyUser" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Require Authentication on Remote Control Page</label>
|
||||
<br />
|
||||
<InputCheckbox @bind-Value="Input.RemoteControlRequiresAuthentication" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.RemoteControlRequiresAuthentication" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Remote Control Session Limit</label>
|
||||
<br />
|
||||
<InputNumber @bind-Value="Input.RemoteControlSessionLimit" class="form-control" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.RemoteControlSessionLimit" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Require 2FA</label>
|
||||
<br />
|
||||
<InputCheckbox @bind-Value="Input.Require2FA" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.Require2FA" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">SMTP Display Name</label>
|
||||
<br />
|
||||
<InputText @bind-Value="Input.SmtpDisplayName" class="form-control" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.SmtpDisplayName" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">SMTP Email</label>
|
||||
<br />
|
||||
<InputText type="email" @bind-Value="Input.SmtpEmail" class="form-control" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.SmtpEmail" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">SMTP Host</label>
|
||||
<br />
|
||||
<InputText @bind-Value="Input.SmtpHost" class="form-control" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.SmtpHost" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">SMTP Port</label>
|
||||
<br />
|
||||
<InputNumber @bind-Value="Input.SmtpPort" class="form-control" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.SmtpPort" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">SMTP Check Certificate Revocation</label>
|
||||
<div class="small text-muted">
|
||||
This sometimes needs to be disabled for Let's Encrypt certificates.
|
||||
</div>
|
||||
<br />
|
||||
<InputCheckbox @bind-Value="Input.SmtpCheckCertificateRevocation" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.SmtpCheckCertificateRevocation" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">SMTP Local Domain</label>
|
||||
<br />
|
||||
<InputText @bind-Value="Input.SmtpLocalDomain" class="form-control" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.SmtpLocalDomain" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">SMTP Username</label>
|
||||
<br />
|
||||
<InputText @bind-Value="Input.SmtpUserName" class="form-control" autocomplete="new-password" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.SmtpUserName" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">SMTP Password</label>
|
||||
<br />
|
||||
<InputText type="password" @bind-Value="Input.SmtpPassword" class="form-control" autocomplete="new-password" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.SmtpPassword" />
|
||||
</div>
|
||||
<div class="form-group text-right">
|
||||
<button id="sendTestEmailButton" type="button" class="btn btn-secondary" @onclick="SaveAndTestSmtpSettings">Test</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Theme</label>
|
||||
<br />
|
||||
<select class="form-control" @bind="Input.Theme">
|
||||
@foreach (var theme in Enum.GetValues<Theme>())
|
||||
{
|
||||
<option @key="theme" value="@theme">@theme</option>
|
||||
}
|
||||
|
||||
</select>
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.Theme" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Trusted CORS Origins</label>
|
||||
<br />
|
||||
<select class="form-control" @bind="_trustedCorsOriginSelected" size="4">
|
||||
@foreach (var origin in Input.TrustedCorsOrigins)
|
||||
{
|
||||
<option @key="origin" value="@origin">@origin</option>
|
||||
}
|
||||
</select>
|
||||
|
||||
<div class="text-right mb-2 mt-1">
|
||||
<button type="button" class="btn btn-secondary" @onclick="RemoveTrustedCorsOrigin">Remove</button>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input placeholder="Add trusted URL"
|
||||
class="form-control"
|
||||
autocomplete="off"
|
||||
@bind="_trustedCorsOriginToAdd"
|
||||
@bind:event="oninput"
|
||||
@onkeydown="HandleTrustedCorsKeyDown" />
|
||||
|
||||
<button id="trustedCorsAddButton" type="button" class="btn btn-secondary" @onclick="AddTrustedCorsOrigin">Add</button>
|
||||
</div>
|
||||
<ValidationMessage For="() => Input.TrustedCorsOrigins" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Use HSTS</label>
|
||||
<br />
|
||||
<InputCheckbox @bind-Value="Input.UseHsts" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.UseHsts" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Use HTTP Logging</label>
|
||||
<br />
|
||||
<InputCheckbox @bind-Value="Input.UseHttpLogging" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.UseHttpLogging" />
|
||||
</div>
|
||||
|
||||
<h4>Connection Strings</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">PostgreSQL</label>
|
||||
<br />
|
||||
<InputText @bind-Value="ConnectionStrings.PostgreSQL" class="form-control" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => ConnectionStrings.PostgreSQL" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">SQLite</label>
|
||||
<br />
|
||||
<InputText @bind-Value="ConnectionStrings.SQLite" class="form-control" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => ConnectionStrings.SQLite" />
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">SQL Server</label>
|
||||
<br />
|
||||
<InputText @bind-Value="ConnectionStrings.SQLServer" class="form-control" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => ConnectionStrings.SQLServer" />
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group mt-3">
|
||||
<button type="button" class="btn btn-primary" @onclick="Save">Save</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<EditForm Model="Input" OnValidSubmit="HandleValidSubmit" autocomplete="off">
|
||||
<DataAnnotationsValidator />
|
||||
|
||||
<h3 class="mt-3">Application Settings</h3>
|
||||
|
||||
<ValidationSummary Model="Input" />
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label>Allow API Login</label>
|
||||
<br />
|
||||
<InputCheckbox @bind-Value="Input.AllowApiLogin" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.AllowApiLogin" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Banned Devices</label>
|
||||
<br />
|
||||
<select size="4" class="form-control" @bind="_bannedDeviceSelected">
|
||||
@foreach (var bannedDevice in Input.BannedDevices)
|
||||
{
|
||||
<option @key="bannedDevice" value="@bannedDevice">@bannedDevice</option>
|
||||
}
|
||||
</select>
|
||||
|
||||
<div class="text-right mb-2 mt-1">
|
||||
<button type="button" class="btn btn-secondary" @onclick="RemoveBannedDevice">Remove</button>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input placeholder="Add banned device ID, name, or IP"
|
||||
class="form-control" autocomplete="off"
|
||||
@bind="_bannedDeviceToAdd"
|
||||
@bind:event="oninput"
|
||||
@onkeydown="HandleBannedDeviceKeyDown" />
|
||||
<button type="button" class="btn btn-secondary" @onclick="AddBannedDevice">Add</button>
|
||||
</div>
|
||||
<ValidationMessage For="() => Input.BannedDevices" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Data Retention in Days</label>
|
||||
<br />
|
||||
<InputNumber @bind-Value="Input.DataRetentionInDays" class="form-control" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.DataRetentionInDays" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Database Provider</label>
|
||||
<br />
|
||||
<select class="form-control" @bind="Input.DBProvider">
|
||||
@foreach (var provider in Enum.GetValues<DbProvider>())
|
||||
{
|
||||
<option @key="provider" value="@provider">@provider</option>
|
||||
}
|
||||
</select>
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.DBProvider" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Enable Windows Event Log</label>
|
||||
<br />
|
||||
<InputCheckbox @bind-Value="Input.EnableWindowsEventLog" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.EnableWindowsEventLog" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Enforce Attended Access</label>
|
||||
<br />
|
||||
<InputCheckbox @bind-Value="Input.EnforceAttendedAccess" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.EnforceAttendedAccess" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Force Client HTTPS</label>
|
||||
<br />
|
||||
<InputCheckbox @bind-Value="Input.ForceClientHttps" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.ForceClientHttps" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Known Proxies</label>
|
||||
<br />
|
||||
<select class="form-control" @bind="_knownProxySelected" size="4">
|
||||
@foreach (var proxy in Input.KnownProxies)
|
||||
{
|
||||
<option @key="proxy" value="@proxy">@proxy</option>
|
||||
}
|
||||
</select>
|
||||
<div class="text-right mb-2 mt-1">
|
||||
<button type="button" class="btn btn-secondary" @onclick="RemoveKnownProxy">Remove</button>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input @bind="_knownProxyToAdd"
|
||||
@bind:event="oninput"
|
||||
placeholder="Add a known proxy"
|
||||
class="form-control"
|
||||
autocomplete="off"
|
||||
@onkeydown="HandleKnownProxyKeyDown" />
|
||||
|
||||
<button type="button" class="btn btn-secondary" @onclick="AddKnownProxy">Add</button>
|
||||
</div>
|
||||
<ValidationMessage For="() => Input.KnownProxies" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Max Organization Count</label>
|
||||
<br />
|
||||
<InputNumber @bind-Value="Input.MaxOrganizationCount" class="form-control" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.MaxOrganizationCount" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Max Concurrent Updates</label>
|
||||
<br />
|
||||
<InputNumber @bind-Value="Input.MaxConcurrentUpdates" class="form-control" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.MaxConcurrentUpdates" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Message of the Day</label>
|
||||
<br />
|
||||
<InputText @bind-Value="Input.MessageOfTheDay" class="form-control" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.MessageOfTheDay" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Redirect to HTTPS</label>
|
||||
<br />
|
||||
<InputCheckbox @bind-Value="Input.RedirectToHttps" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.RedirectToHttps" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Remote Control Notify User</label>
|
||||
<br />
|
||||
<InputCheckbox @bind-Value="Input.RemoteControlNotifyUser" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.RemoteControlNotifyUser" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Require Authentication on Remote Control Page</label>
|
||||
<br />
|
||||
<InputCheckbox @bind-Value="Input.RemoteControlRequiresAuthentication" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.RemoteControlRequiresAuthentication" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Remote Control Session Limit</label>
|
||||
<br />
|
||||
<InputNumber @bind-Value="Input.RemoteControlSessionLimit" class="form-control" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.RemoteControlSessionLimit" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Require 2FA</label>
|
||||
<br />
|
||||
<InputCheckbox @bind-Value="Input.Require2FA" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.Require2FA" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">SMTP Display Name</label>
|
||||
<br />
|
||||
<InputText @bind-Value="Input.SmtpDisplayName" class="form-control" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.SmtpDisplayName" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">SMTP Email</label>
|
||||
<br />
|
||||
<InputText type="email" @bind-Value="Input.SmtpEmail" class="form-control" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.SmtpEmail" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">SMTP Host</label>
|
||||
<br />
|
||||
<InputText @bind-Value="Input.SmtpHost" class="form-control" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.SmtpHost" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">SMTP Port</label>
|
||||
<br />
|
||||
<InputNumber @bind-Value="Input.SmtpPort" class="form-control" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.SmtpPort" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">SMTP Check Certificate Revocation</label>
|
||||
<div class="small text-muted">
|
||||
This sometimes needs to be disabled for Let's Encrypt certificates.
|
||||
</div>
|
||||
<br />
|
||||
<InputCheckbox @bind-Value="Input.SmtpCheckCertificateRevocation" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.SmtpCheckCertificateRevocation" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">SMTP Local Domain</label>
|
||||
<br />
|
||||
<InputText @bind-Value="Input.SmtpLocalDomain" class="form-control" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.SmtpLocalDomain" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">SMTP Username</label>
|
||||
<br />
|
||||
<InputText @bind-Value="Input.SmtpUserName" class="form-control" autocomplete="new-password" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.SmtpUserName" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">SMTP Password</label>
|
||||
<br />
|
||||
<InputText type="password" @bind-Value="Input.SmtpPassword" class="form-control" autocomplete="new-password" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.SmtpPassword" />
|
||||
</div>
|
||||
<div class="form-group text-right">
|
||||
<button id="sendTestEmailButton" type="button" class="btn btn-secondary" @onclick="SaveAndTestSmtpSettings">Test</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Theme</label>
|
||||
<br />
|
||||
<select class="form-control" @bind="Input.Theme">
|
||||
@foreach (var theme in Enum.GetValues<Theme>())
|
||||
{
|
||||
<option @key="theme" value="@theme">@theme</option>
|
||||
}
|
||||
|
||||
</select>
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.Theme" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Trusted CORS Origins</label>
|
||||
<br />
|
||||
<select class="form-control" @bind="_trustedCorsOriginSelected" size="4">
|
||||
@foreach (var origin in Input.TrustedCorsOrigins)
|
||||
{
|
||||
<option @key="origin" value="@origin">@origin</option>
|
||||
}
|
||||
</select>
|
||||
|
||||
<div class="text-right mb-2 mt-1">
|
||||
<button type="button" class="btn btn-secondary" @onclick="RemoveTrustedCorsOrigin">Remove</button>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input placeholder="Add trusted URL"
|
||||
class="form-control"
|
||||
autocomplete="off"
|
||||
@bind="_trustedCorsOriginToAdd"
|
||||
@bind:event="oninput"
|
||||
@onkeydown="HandleTrustedCorsKeyDown" />
|
||||
|
||||
<button id="trustedCorsAddButton" type="button" class="btn btn-secondary" @onclick="AddTrustedCorsOrigin">Add</button>
|
||||
</div>
|
||||
<ValidationMessage For="() => Input.TrustedCorsOrigins" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Use HSTS</label>
|
||||
<br />
|
||||
<InputCheckbox @bind-Value="Input.UseHsts" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.UseHsts" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Use HTTP Logging</label>
|
||||
<br />
|
||||
<InputCheckbox @bind-Value="Input.UseHttpLogging" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => Input.UseHttpLogging" />
|
||||
</div>
|
||||
|
||||
<h4>Connection Strings</h4>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">PostgreSQL</label>
|
||||
<br />
|
||||
<InputText @bind-Value="ConnectionStrings.PostgreSQL" class="form-control" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => ConnectionStrings.PostgreSQL" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">SQLite</label>
|
||||
<br />
|
||||
<InputText @bind-Value="ConnectionStrings.SQLite" class="form-control" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => ConnectionStrings.SQLite" />
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label">SQL Server</label>
|
||||
<br />
|
||||
<InputText @bind-Value="ConnectionStrings.SQLServer" class="form-control" autocomplete="off" />
|
||||
<br />
|
||||
<ValidationMessage For="() => ConnectionStrings.SQLServer" />
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group mt-3">
|
||||
<button type="button" class="btn btn-primary" @onclick="Save">Save</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
<h5 class="text-muted">Only organization administrators can view this page.</h5>
|
||||
}
|
||||
</div>
|
||||
@ -1,6 +1,6 @@
|
||||
@page "/server-logs"
|
||||
@using System.Collections.ObjectModel;
|
||||
@attribute [Authorize]
|
||||
@attribute [Authorize(Policy = PolicyNames.ServerAdminRequired)]
|
||||
@inherits AuthComponentBase
|
||||
|
||||
@inject IDataService DataService
|
||||
@ -11,65 +11,57 @@
|
||||
|
||||
<h3 class="mb-3">Server Logs</h3>
|
||||
|
||||
|
||||
@if (User?.IsAdministrator == true)
|
||||
{
|
||||
<div class="buttons-row mb-3">
|
||||
<div>
|
||||
<button class="btn btn-danger" type="button" @onclick="ClearAllLogs">
|
||||
Delete Logs
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-primary"
|
||||
onclick="window.open('/API/ServerLogs/Download', '_blank')">
|
||||
Download Logs
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-primary"
|
||||
onclick="window.open('/API/ScriptResults', '_blank')">
|
||||
Download Script History
|
||||
</button>
|
||||
</div>
|
||||
<div class="buttons-row mb-3">
|
||||
<div>
|
||||
<button class="btn btn-danger" type="button" @onclick="ClearAllLogs">
|
||||
Delete Logs
|
||||
</button>
|
||||
</div>
|
||||
<div class="filters-row mb-3">
|
||||
<div style="display:inline-block">
|
||||
<strong>Type:</strong>
|
||||
<br />
|
||||
<select class="form-control-sm" @bind="LogLevel">
|
||||
<option value="">All</option>
|
||||
@foreach (var eventType in Enum.GetValues(typeof(LogLevel)))
|
||||
{
|
||||
<option @key="eventType" value="@eventType">@eventType</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div style="display:inline-block">
|
||||
<strong>Filter:</strong>
|
||||
<br />
|
||||
<input type="text" @bind="MessageFilter" @bind:event="oninput" />
|
||||
</div>
|
||||
<div style="display:inline-block">
|
||||
<strong>From:</strong>
|
||||
<br />
|
||||
<input type="date" @bind="FromDate" />
|
||||
</div>
|
||||
<div style="display:inline-block">
|
||||
<strong>To:</strong>
|
||||
<br />
|
||||
<input type="date" @bind="ToDate" />
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-primary"
|
||||
onclick="window.open('/API/ServerLogs/Download', '_blank')">
|
||||
Download Logs
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-primary"
|
||||
onclick="window.open('/API/ScriptResults', '_blank')">
|
||||
Download Script History
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filters-row mb-3">
|
||||
<div style="display:inline-block">
|
||||
<strong>Type:</strong>
|
||||
<br />
|
||||
<select class="form-control-sm" @bind="LogLevel">
|
||||
<option value="">All</option>
|
||||
@foreach (var eventType in Enum.GetValues(typeof(LogLevel)))
|
||||
{
|
||||
<option @key="eventType" value="@eventType">@eventType</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div style="display:inline-block">
|
||||
<strong>Filter:</strong>
|
||||
<br />
|
||||
<input type="text" @bind="MessageFilter" @bind:event="oninput" />
|
||||
</div>
|
||||
<div style="display:inline-block">
|
||||
<strong>From:</strong>
|
||||
<br />
|
||||
<input type="date" @bind="FromDate" />
|
||||
</div>
|
||||
<div style="display:inline-block">
|
||||
<strong>To:</strong>
|
||||
<br />
|
||||
<input type="date" @bind="ToDate" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<textarea readonly class="logs-content bg-dark text-white">
|
||||
@(string.Join(Environment.NewLine, _filteredLogs))
|
||||
</textarea>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h5 class="text-muted">Only organization administrators can view this page.</h5>
|
||||
}
|
||||
<textarea readonly class="logs-content bg-dark text-white">
|
||||
@(string.Join(Environment.NewLine, _filteredLogs))
|
||||
</textarea>
|
||||
|
||||
@code {
|
||||
private readonly string _messageDebounceKey = Guid.NewGuid().ToString();
|
||||
@ -98,14 +90,14 @@ else
|
||||
private LogLevel? LogLevel
|
||||
{
|
||||
get => _logLevel;
|
||||
set
|
||||
set
|
||||
{
|
||||
_logLevel = value;
|
||||
_ = RefreshLogs();
|
||||
}
|
||||
}
|
||||
|
||||
private string MessageFilter
|
||||
private string MessageFilter
|
||||
{
|
||||
get => _messageFilter;
|
||||
set
|
||||
@ -113,7 +105,7 @@ else
|
||||
_messageFilter = value;
|
||||
|
||||
Debouncer.Debounce(
|
||||
TimeSpan.FromSeconds(1),
|
||||
TimeSpan.FromSeconds(1),
|
||||
() => _ = RefreshLogs(),
|
||||
_messageDebounceKey);
|
||||
}
|
||||
@ -136,10 +128,20 @@ else
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await RefreshLogs();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
if (firstRender)
|
||||
{
|
||||
await RefreshLogs();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private async Task ClearAllLogs()
|
||||
{
|
||||
var result = await JsInterop.Confirm("Are you sure you want to delete all previous logs? Today's logs are retained.");
|
||||
|
||||
@ -100,12 +100,25 @@ services.AddIdentity<RemotelyUser, IdentityRole>(options =>
|
||||
|
||||
|
||||
services.AddScoped<IAuthorizationHandler, TwoFactorRequiredHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, OrganizationAdminRequirementHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, ServerAdminRequirementHandler>();
|
||||
|
||||
services.AddAuthorization(options =>
|
||||
{
|
||||
options.AddPolicy(TwoFactorRequiredRequirement.PolicyName, builder =>
|
||||
options.AddPolicy(PolicyNames.TwoFactorRequired, builder =>
|
||||
{
|
||||
builder.Requirements.Add(new TwoFactorRequiredRequirement());
|
||||
});
|
||||
|
||||
options.AddPolicy(PolicyNames.OrganizationAdminRequired, builder =>
|
||||
{
|
||||
builder.Requirements.Add(new OrganizationAdminRequirement());
|
||||
});
|
||||
|
||||
options.AddPolicy(PolicyNames.ServerAdminRequired, builder =>
|
||||
{
|
||||
builder.Requirements.Add(new ServerAdminRequirement());
|
||||
});
|
||||
});
|
||||
|
||||
services.AddRazorPages();
|
||||
@ -337,13 +350,16 @@ void ConfigureSerilog(WebApplicationBuilder webAppBuilder)
|
||||
{
|
||||
loggerConfiguration
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.Console()
|
||||
.Enrich.WithThreadId()
|
||||
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties}{NewLine}{Exception}")
|
||||
.WriteTo.File($"{logPath}/Remotely_Server.log",
|
||||
rollingInterval: RollingInterval.Day,
|
||||
retainedFileTimeLimit: TimeSpan.FromDays(dataRetentionDays),
|
||||
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj} {Properties}{NewLine}{Exception}",
|
||||
shared: true);
|
||||
}
|
||||
|
||||
// https://github.com/serilog/serilog-aspnetcore#two-stage-initialization
|
||||
var loggerConfig = new LoggerConfiguration();
|
||||
ApplySharedLoggerConfig(loggerConfig);
|
||||
Log.Logger = loggerConfig.CreateBootstrapLogger();
|
||||
|
||||
@ -34,6 +34,7 @@
|
||||
<PackageReference Include="Nihs.SimpleMessenger" Version="1.0.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.4" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="7.0.0" />
|
||||
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
@using Remotely.Server.Auth
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<AuthorizeView Policy="@TwoFactorRequiredRequirement.PolicyName">
|
||||
<AuthorizeView Policy="@PolicyNames.TwoFactorRequired">
|
||||
<Authorized>
|
||||
<div class="page">
|
||||
<div class="sidebar">
|
||||
|
||||
@ -22,4 +22,5 @@
|
||||
@using Remotely.Server.Components.TabControl
|
||||
@using System.Collections.Concurrent
|
||||
@using Remotely.Server.Components.Scripts
|
||||
@using Remotely.Server.Components.TreeView
|
||||
@using Remotely.Server.Components.TreeView
|
||||
@using Remotely.Server.Auth;
|
||||
Loading…
Reference in New Issue
Block a user