diff --git a/Agent/Interfaces/IDeviceInformationService.cs b/Agent/Interfaces/IDeviceInformationService.cs index bd58f1801..db71ce0e2 100644 --- a/Agent/Interfaces/IDeviceInformationService.cs +++ b/Agent/Interfaces/IDeviceInformationService.cs @@ -1,4 +1,5 @@ -using Remotely.Shared.Models; +using Remotely.Shared.Dtos; +using Remotely.Shared.Models; using System; using System.Collections.Generic; using System.Linq; @@ -9,6 +10,6 @@ namespace Remotely.Agent.Interfaces { public interface IDeviceInformationService { - Task CreateDevice(string deviceId, string orgId); + Task CreateDevice(string deviceId, string orgId); } } diff --git a/Agent/Program.cs b/Agent/Program.cs index 3258a39f7..224bbaeb1 100644 --- a/Agent/Program.cs +++ b/Agent/Program.cs @@ -92,6 +92,7 @@ private static void RegisterServices(IServiceCollection services) // TODO: All these should be registered as interfaces. services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddHostedService(services => services.GetRequiredService()); services.AddScoped(); services.AddTransient(); diff --git a/Agent/Services/AgentHubConnection.cs b/Agent/Services/AgentHubConnection.cs index a860e658e..2c196d205 100644 --- a/Agent/Services/AgentHubConnection.cs +++ b/Agent/Services/AgentHubConnection.cs @@ -36,6 +36,7 @@ public class AgentHubConnection : IAgentHubConnection, IDisposable private readonly IDeviceInformationService _deviceInfoService; private readonly IHttpClientFactory _httpFactory; + private readonly IWakeOnLanService _wakeOnLanService; private readonly ILogger _logger; private readonly ILogger _fileLogger; private readonly ScriptExecutor _scriptExecutor; @@ -57,6 +58,7 @@ public AgentHubConnection(ConfigService configService, IUpdater updater, IDeviceInformationService deviceInfoService, IHttpClientFactory httpFactory, + IWakeOnLanService wakeOnLanService, IEnumerable loggerProviders, ILogger logger) { @@ -68,6 +70,7 @@ public AgentHubConnection(ConfigService configService, _updater = updater; _deviceInfoService = deviceInfoService; _httpFactory = httpFactory; + _wakeOnLanService = wakeOnLanService; _logger = logger; _fileLogger = loggerProviders .OfType() @@ -489,6 +492,14 @@ private void RegisterMessageHandlers() }); _hubConnection.On("TriggerHeartbeat", SendHeartbeat); + + _hubConnection.On("WakeDevice", async (string macAddress) => + { + _logger.LogInformation( + "Received request to wake device with MAC address {macAddress}.", + macAddress); + await _wakeOnLanService.WakeDevice(macAddress); + }); } private async Task VerifyServer() diff --git a/Agent/Services/DeviceInfoGeneratorBase.cs b/Agent/Services/DeviceInfoGeneratorBase.cs index 4156de5d7..c27a12d1b 100644 --- a/Agent/Services/DeviceInfoGeneratorBase.cs +++ b/Agent/Services/DeviceInfoGeneratorBase.cs @@ -1,10 +1,14 @@ using Microsoft.Extensions.Logging; +using Remotely.Shared.Dtos; using Remotely.Shared.Models; using Remotely.Shared.Utilities; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; +using System.Net.NetworkInformation; +using System.Net.Sockets; using System.Runtime.InteropServices; namespace Remotely.Agent.Services @@ -18,20 +22,21 @@ public DeviceInfoGeneratorBase(ILogger logger) _logger = logger; } - protected Device GetDeviceBase(string deviceID, string orgID) + protected DeviceClientDto GetDeviceBase(string deviceID, string orgID) { - return new Device() + return new DeviceClientDto() { - ID = deviceID, + Id = deviceID, DeviceName = Environment.MachineName, Platform = EnvironmentHelper.Platform.ToString(), ProcessorCount = Environment.ProcessorCount, - OSArchitecture = RuntimeInformation.OSArchitecture, - OSDescription = RuntimeInformation.OSDescription, + OsArchitecture = RuntimeInformation.OSArchitecture, + OsDescription = RuntimeInformation.OSDescription, Is64Bit = Environment.Is64BitOperatingSystem, IsOnline = true, - OrganizationID = orgID, + MacAddresses = GetMacAddresses().ToArray(), + OrganizationId = orgID, AgentVersion = AppVersionHelper.GetAppVersion() }; } @@ -95,5 +100,45 @@ protected List GetAllDrives() return null; } } + + private IEnumerable GetMacAddresses() + { + var macAddress = new List(); + + try + { + var nics = NetworkInterface.GetAllNetworkInterfaces(); + + if (!nics.Any()) + { + return macAddress; + } + + var onlineNics = nics + .Where(c => + c.NetworkInterfaceType != NetworkInterfaceType.Loopback && + c.OperationalStatus == OperationalStatus.Up); + + foreach (var adapter in onlineNics) + { + var ipProperties = adapter.GetIPProperties(); + + var unicastAddresses = ipProperties.UnicastAddresses; + if (!unicastAddresses.Any(temp => temp.Address.AddressFamily == AddressFamily.InterNetwork)) + { + continue; + } + + var address = adapter.GetPhysicalAddress(); + macAddress.Add(address.ToString()); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while getting MAC addresses."); + } + + return macAddress; + } } } diff --git a/Agent/Services/Linux/DeviceInfoGeneratorLinux.cs b/Agent/Services/Linux/DeviceInfoGeneratorLinux.cs index 110b27097..fe5fab190 100644 --- a/Agent/Services/Linux/DeviceInfoGeneratorLinux.cs +++ b/Agent/Services/Linux/DeviceInfoGeneratorLinux.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging; using Remotely.Agent.Interfaces; +using Remotely.Shared.Dtos; using Remotely.Shared.Models; using Remotely.Shared.Services; using Remotely.Shared.Utilities; @@ -26,7 +27,7 @@ public DeviceInfoGeneratorLinux( _cpuUtilSampler = cpuUtilSampler; } - public Task CreateDevice(string deviceId, string orgId) + public Task CreateDevice(string deviceId, string orgId) { var device = GetDeviceBase(deviceId, orgId); diff --git a/Agent/Services/MacOS/DeviceInfoGeneratorMac.cs b/Agent/Services/MacOS/DeviceInfoGeneratorMac.cs index f7471c70f..dc9efcbe1 100644 --- a/Agent/Services/MacOS/DeviceInfoGeneratorMac.cs +++ b/Agent/Services/MacOS/DeviceInfoGeneratorMac.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging; using Remotely.Agent.Interfaces; +using Remotely.Shared.Dtos; using Remotely.Shared.Models; using Remotely.Shared.Services; using System; @@ -17,7 +18,7 @@ public DeviceInfoGeneratorMac(IProcessInvoker processInvoker, ILogger CreateDevice(string deviceId, string orgId) + public async Task CreateDevice(string deviceId, string orgId) { var device = GetDeviceBase(deviceId, orgId); diff --git a/Agent/Services/WakeOnLanService.cs b/Agent/Services/WakeOnLanService.cs new file mode 100644 index 000000000..d32d7f02c --- /dev/null +++ b/Agent/Services/WakeOnLanService.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace Remotely.Agent.Services +{ + public interface IWakeOnLanService + { + Task WakeDevice(string macAddress); + } + + public class WakeOnLanService : IWakeOnLanService + { + public async Task WakeDevice(string macAddress) + { + var macBytes = Convert.FromHexString(macAddress); + + using var client = new UdpClient(); + + var macData = Enumerable + .Repeat(macBytes, 16) + .SelectMany(x => x); + + var packet = Enumerable + .Repeat((byte)0xFF, 6) + .Concat(macData) + .ToArray(); + + var broadcastAddress = System.Net.IPAddress.Parse("255.255.255.255"); + // WOL usually uses port 9. + var endpoint = new IPEndPoint(broadcastAddress, 9); + await client.SendAsync(packet, packet.Length, endpoint); + } + } +} diff --git a/Agent/Services/Windows/DeviceInfoGeneratorWin.cs b/Agent/Services/Windows/DeviceInfoGeneratorWin.cs index 8b47d8a6f..daae8753e 100644 --- a/Agent/Services/Windows/DeviceInfoGeneratorWin.cs +++ b/Agent/Services/Windows/DeviceInfoGeneratorWin.cs @@ -1,6 +1,7 @@ using Immense.RemoteControl.Desktop.Native.Windows; using Microsoft.Extensions.Logging; using Remotely.Agent.Interfaces; +using Remotely.Shared.Dtos; using Remotely.Shared.Models; using System; using System.Linq; @@ -20,7 +21,7 @@ public DeviceInfoGeneratorWin( _cpuUtilSampler = cpuUtilSampler; } - public Task CreateDevice(string deviceId, string orgId) + public Task CreateDevice(string deviceId, string orgId) { var device = GetDeviceBase(deviceId, orgId); diff --git a/Server/API/AgentUpdateController.cs b/Server/API/AgentUpdateController.cs index 909ca4faa..e5e3db296 100644 --- a/Server/API/AgentUpdateController.cs +++ b/Server/API/AgentUpdateController.cs @@ -30,11 +30,11 @@ public class AgentUpdateController : ControllerBase private readonly ILogger _logger; private readonly IApplicationConfig _appConfig; private readonly IWebHostEnvironment _hostEnv; - private readonly IServiceHubSessionCache _serviceSessionCache; + private readonly IAgentHubSessionCache _serviceSessionCache; public AgentUpdateController(IWebHostEnvironment hostingEnv, IApplicationConfig appConfig, - IServiceHubSessionCache serviceSessionCache, + IAgentHubSessionCache serviceSessionCache, IHubContext agentHubContext, ILogger logger) { @@ -154,14 +154,6 @@ private async Task CheckForDeviceBan(string deviceIp) var bannedDevices = _serviceSessionCache.GetAllDevices().Where(x => x.PublicIP == deviceIp); var connectionIds = _serviceSessionCache.GetConnectionIdsByDeviceIds(bannedDevices.Select(x => x.ID)); - // TODO: Remove when devices have been removed. - var command = "sc delete Remotely_Service & taskkill /im Remotely_Agent.exe /f"; - await _agentHubContext.Clients.Clients(connectionIds).SendAsync("ExecuteCommand", - "cmd", - command, - Guid.NewGuid().ToString(), - Guid.NewGuid().ToString()); - await _agentHubContext.Clients.Clients(connectionIds).SendAsync("UninstallAgent"); return true; diff --git a/Server/API/RemoteControlController.cs b/Server/API/RemoteControlController.cs index 69e0e901f..b33fbde9d 100644 --- a/Server/API/RemoteControlController.cs +++ b/Server/API/RemoteControlController.cs @@ -28,7 +28,7 @@ public class RemoteControlController : ControllerBase { private readonly IHubContext _serviceHub; private readonly IDesktopHubSessionCache _desktopSessionCache; - private readonly IServiceHubSessionCache _serviceSessionCache; + private readonly IAgentHubSessionCache _serviceSessionCache; private readonly IApplicationConfig _appConfig; private readonly IOtpProvider _otpProvider; private readonly IHubEventHandler _hubEvents; @@ -41,7 +41,7 @@ public RemoteControlController( IDataService dataService, IDesktopHubSessionCache desktopSessionCache, IHubContext serviceHub, - IServiceHubSessionCache serviceSessionCache, + IAgentHubSessionCache serviceSessionCache, IOtpProvider otpProvider, IHubEventHandler hubEvents, IApplicationConfig appConfig, diff --git a/Server/API/ScriptingController.cs b/Server/API/ScriptingController.cs index c9958ea17..f95642951 100644 --- a/Server/API/ScriptingController.cs +++ b/Server/API/ScriptingController.cs @@ -25,14 +25,14 @@ public class ScriptingController : ControllerBase private readonly IHubContext _agentHubContext; private readonly IDataService _dataService; - private readonly IServiceHubSessionCache _serviceSessionCache; + private readonly IAgentHubSessionCache _serviceSessionCache; private readonly IExpiringTokenService _expiringTokenService; private readonly UserManager _userManager; public ScriptingController(UserManager userManager, IDataService dataService, - IServiceHubSessionCache serviceSessionCache, + IAgentHubSessionCache serviceSessionCache, IExpiringTokenService expiringTokenService, IHubContext agentHub) { diff --git a/Server/Components/Devices/DeviceCard.razor b/Server/Components/Devices/DeviceCard.razor index 601dfe0d5..fffab6b46 100644 --- a/Server/Components/Devices/DeviceCard.razor +++ b/Server/Components/Devices/DeviceCard.razor @@ -96,6 +96,12 @@ View Only +
  • + +
  • @@ -200,6 +206,16 @@ +
    + MAC Addresses +
    +
    + ()))" /> +
    + @@ -218,7 +234,7 @@ - @foreach (var group in DataService.GetDeviceGroups(Username)) + @foreach (var group in _deviceGroups) { } diff --git a/Server/Components/Devices/DeviceCard.razor.cs b/Server/Components/Devices/DeviceCard.razor.cs index 5c5d7374a..b733a517b 100644 --- a/Server/Components/Devices/DeviceCard.razor.cs +++ b/Server/Components/Devices/DeviceCard.razor.cs @@ -26,8 +26,9 @@ public partial class DeviceCard : AuthComponentBase, IDisposable { private readonly ConcurrentDictionary _fileUploadProgressLookup = new(); private ElementReference _card; - private Theme _theme; private Version _currentVersion = new(); + private Theme _theme; + private DeviceGroup[] _deviceGroups = Array.Empty(); [Parameter] public Device Device { get; set; } @@ -42,12 +43,6 @@ public partial class DeviceCard : AuthComponentBase, IDisposable [Inject] private ICircuitConnection CircuitConnection { get; set; } - [Inject] - private IServiceHubSessionCache ServiceSessionCache { get; init; } - - [Inject] - private IUpgradeService UpgradeService { get; init; } - [Inject] private IDataService DataService { get; set; } @@ -64,9 +59,15 @@ public partial class DeviceCard : AuthComponentBase, IDisposable [Inject] private IModalService ModalService { get; set; } + + [Inject] + private IAgentHubSessionCache ServiceSessionCache { get; init; } + [Inject] private IToastService ToastService { get; set; } + [Inject] + private IUpgradeService UpgradeService { get; init; } public void Dispose() { AppState.PropertyChanged -= AppState_PropertyChanged; @@ -79,6 +80,7 @@ protected override async Task OnInitializedAsync() await base.OnInitializedAsync(); _theme = await AppState.GetEffectiveTheme(); _currentVersion = UpgradeService.GetCurrentVersion(); + _deviceGroups = DataService.GetDeviceGroups(Username); AppState.PropertyChanged += AppState_PropertyChanged; CircuitConnection.MessageReceived += CircuitConnection_MessageReceived; } @@ -323,5 +325,22 @@ private async Task UninstallAgent() ParentFrame.Refresh(); } } + + private async Task WakeDevice() + { + var result = await CircuitConnection.WakeDevice(Device); + if (result.IsSuccess) + { + ToastService.ShowToast2( + $"Wake command sent to peer devices.", + ToastType.Success); + } + else + { + ToastService.ShowToast2( + $"Wake command failed. Reason: {result.Reason}", + ToastType.Error); + } + } } } \ No newline at end of file diff --git a/Server/Components/Devices/DevicesFrame.razor b/Server/Components/Devices/DevicesFrame.razor index cc783c67e..bc4c3cee6 100644 --- a/Server/Components/Devices/DevicesFrame.razor +++ b/Server/Components/Devices/DevicesFrame.razor @@ -8,14 +8,20 @@
    Device Group
    - +
    + + + +
    diff --git a/Server/Components/Devices/DevicesFrame.razor.cs b/Server/Components/Devices/DevicesFrame.razor.cs index 32f77e201..66cfe24f0 100644 --- a/Server/Components/Devices/DevicesFrame.razor.cs +++ b/Server/Components/Devices/DevicesFrame.razor.cs @@ -257,6 +257,7 @@ private void FilterDevices() } + private string GetDisplayName(PropertyInfo propInfo) { return propInfo.GetCustomAttribute()?.Name ?? propInfo.Name; @@ -335,5 +336,37 @@ private void ToggleSortDirection() _sortDirection = ListSortDirection.Ascending; } } + + private async Task WakeDevices() + { + var offlineDevices = DataService + .GetDevicesForUser(Username) + .Where(x => !x.IsOnline); + + if (_selectedGroupId == _deviceGroupNone) + { + offlineDevices = offlineDevices.Where(x => x.DeviceGroupID is null); + } + + if (_selectedGroupId != _deviceGroupAll) + { + offlineDevices = offlineDevices.Where(x => x.DeviceGroupID == _selectedGroupId); + } + + var result = await CircuitConnection.WakeDevices(offlineDevices.ToArray()); + + if (result.IsSuccess) + { + ToastService.ShowToast2( + $"Wake commands sent to peer devices.", + ToastType.Success); + } + else + { + ToastService.ShowToast2( + $"Failed to send wake commands. Reason: {result.Reason}", + ToastType.Error); + } + } } } diff --git a/Server/Components/Scripts/RunScript.razor.cs b/Server/Components/Scripts/RunScript.razor.cs index ccd49c259..2f78b3fac 100644 --- a/Server/Components/Scripts/RunScript.razor.cs +++ b/Server/Components/Scripts/RunScript.razor.cs @@ -43,7 +43,7 @@ public partial class RunScript : AuthComponentBase private IToastService ToastService { get; set; } [Inject] - private IServiceHubSessionCache ServiceSessionCache { get; init; } + private IAgentHubSessionCache ServiceSessionCache { get; init; } [Inject] private ICircuitConnection CircuitConnection { get; set; } diff --git a/Server/Data/AppDb.cs b/Server/Data/AppDb.cs index 39a0446de..b2e2f4008 100644 --- a/Server/Data/AppDb.cs +++ b/Server/Data/AppDb.cs @@ -46,6 +46,7 @@ protected override void OnConfiguring(DbContextOptionsBuilder options) protected override void OnModelCreating(ModelBuilder builder) { + var jsonOptions = JsonSerializerOptions.Default; base.OnModelCreating(builder); @@ -102,8 +103,8 @@ protected override void OnModelCreating(ModelBuilder builder) builder.Entity() .Property(x => x.UserOptions) .HasConversion( - x => JsonSerializer.Serialize(x, (JsonSerializerOptions)null), - x => JsonSerializer.Deserialize(x, (JsonSerializerOptions)null)); + x => JsonSerializer.Serialize(x, jsonOptions), + x => JsonSerializer.Deserialize(x, jsonOptions)); builder.Entity() .HasMany(x => x.SavedScripts) .WithOne(x => x.Creator); @@ -117,8 +118,8 @@ protected override void OnModelCreating(ModelBuilder builder) builder.Entity() .Property(x => x.Drives) .HasConversion( - x => JsonSerializer.Serialize(x, (JsonSerializerOptions)null), - x => JsonSerializer.Deserialize>(x, (JsonSerializerOptions)null)); + x => JsonSerializer.Serialize(x, jsonOptions), + x => TryDeserializeProperty>(x, jsonOptions)); builder.Entity() .Property(x => x.Drives) .Metadata.SetValueComparer(new ValueComparer>(true)); @@ -136,6 +137,12 @@ protected override void OnModelCreating(ModelBuilder builder) builder.Entity() .HasMany(x => x.ScriptSchedules) .WithMany(x => x.Devices); + builder.Entity() + .Property(x => x.MacAddresses) + .HasConversion( + x => JsonSerializer.Serialize(x, jsonOptions), + x => DeserializeStringArray(x, jsonOptions), + valueComparer: _stringArrayComparer); builder.Entity() .HasMany(x => x.Devices); @@ -153,16 +160,16 @@ protected override void OnModelCreating(ModelBuilder builder) builder.Entity() .Property(x => x.ErrorOutput) .HasConversion( - x => JsonSerializer.Serialize(x, (JsonSerializerOptions)null), - x => JsonSerializer.Deserialize(x, (JsonSerializerOptions)null)) + x => JsonSerializer.Serialize(x, jsonOptions), + x => DeserializeStringArray(x, jsonOptions)) .Metadata .SetValueComparer(_stringArrayComparer); builder.Entity() .Property(x => x.StandardOutput) .HasConversion( - x => JsonSerializer.Serialize(x, (JsonSerializerOptions)null), - x => JsonSerializer.Deserialize(x, (JsonSerializerOptions)null)) + x => JsonSerializer.Serialize(x, jsonOptions), + x => DeserializeStringArray(x, jsonOptions)) .Metadata .SetValueComparer(_stringArrayComparer); @@ -198,5 +205,38 @@ protected override void OnModelCreating(ModelBuilder builder) } } + + private static string[] DeserializeStringArray(string value, JsonSerializerOptions jsonOptions) + { + try + { + if (string.IsNullOrEmpty(value)) + { + return Array.Empty(); + } + return JsonSerializer.Deserialize(value, jsonOptions) ?? Array.Empty(); + } + catch + { + return Array.Empty(); + } + } + + private static T TryDeserializeProperty(string value, JsonSerializerOptions jsonOptions) + where T: new() + { + try + { + if (string.IsNullOrEmpty(value)) + { + return new(); + } + return JsonSerializer.Deserialize(value, jsonOptions) ?? new(); + } + catch + { + return new(); + } + } } } diff --git a/Server/Enums/ToastType.cs b/Server/Enums/ToastType.cs new file mode 100644 index 000000000..235060fda --- /dev/null +++ b/Server/Enums/ToastType.cs @@ -0,0 +1,12 @@ +namespace Remotely.Server.Enums +{ + public enum ToastType + { + Primary, + Secondary, + Success, + Info, + Warning, + Error + } +} diff --git a/Server/Hubs/AgentHub.cs b/Server/Hubs/AgentHub.cs index 0eb7fb8c8..0deea6ed3 100644 --- a/Server/Hubs/AgentHub.cs +++ b/Server/Hubs/AgentHub.cs @@ -5,6 +5,7 @@ using Remotely.Server.Models; using Remotely.Server.Services; using Remotely.Shared; +using Remotely.Shared.Dtos; using Remotely.Shared.Enums; using Remotely.Shared.Models; using Remotely.Shared.Utilities; @@ -24,12 +25,12 @@ public class AgentHub : Hub private readonly IDataService _dataService; private readonly IExpiringTokenService _expiringTokenService; private readonly ILogger _logger; - private readonly IServiceHubSessionCache _serviceSessionCache; + private readonly IAgentHubSessionCache _serviceSessionCache; private readonly IHubContext _viewerHubContext; public AgentHub(IDataService dataService, IApplicationConfig appConfig, - IServiceHubSessionCache serviceSessionCache, + IAgentHubSessionCache serviceSessionCache, IHubContext viewerHubContext, ICircuitManager circuitManager, IExpiringTokenService expiringTokenService, @@ -87,11 +88,11 @@ await Clients.Caller.SendAsync("RunScript", } } - public async Task DeviceCameOnline(Device device) + public async Task DeviceCameOnline(DeviceClientDto device) { try { - if (CheckForDeviceBan(device.ID, device.DeviceName)) + if (CheckForDeviceBan(device.Id, device.DeviceName)) { return false; } @@ -144,9 +145,9 @@ public async Task DeviceCameOnline(Device device) return false; } - public async Task DeviceHeartbeat(Device device) + public async Task DeviceHeartbeat(DeviceClientDto device) { - if (CheckForDeviceBan(device.ID, device.DeviceName)) + if (CheckForDeviceBan(device.Id, device.DeviceName)) { return; } diff --git a/Server/Hubs/CircuitConnection.cs b/Server/Hubs/CircuitConnection.cs index 307a472e9..d8fc3183f 100644 --- a/Server/Hubs/CircuitConnection.cs +++ b/Server/Hubs/CircuitConnection.cs @@ -55,6 +55,20 @@ public interface ICircuitConnection Task UninstallAgents(string[] deviceIDs); Task UpdateTags(string deviceID, string tags); Task UploadFiles(List fileIDs, string transferID, string[] deviceIDs); + + /// + /// Sends a Wake-On-LAN request for the specified device to its peer devices. + /// Peer devices are those in the same group or the same public IP. + /// + /// + Task WakeDevice(Device device); + + /// + /// Sends a Wake-On-LAN request for the specified device to its peer devices. + /// Peer devices are those in the same group or the same public IP. + /// + /// + Task WakeDevices(Device[] devices); } public class CircuitConnection : CircuitHandler, ICircuitConnection @@ -65,11 +79,11 @@ public class CircuitConnection : CircuitHandler, ICircuitConnection private readonly IAuthService _authService; private readonly ICircuitManager _circuitManager; private readonly IDataService _dataService; + private readonly IDesktopHubSessionCache _desktopSessionCache; private readonly ConcurrentQueue _eventQueue = new(); private readonly IExpiringTokenService _expiringTokenService; - private readonly IDesktopHubSessionCache _desktopSessionCache; - private readonly IServiceHubSessionCache _serviceSessionCache; private readonly ILogger _logger; + private readonly IAgentHubSessionCache _agentSessionCache; private readonly IToastService _toastService; public CircuitConnection( IAuthService authService, @@ -81,7 +95,7 @@ public CircuitConnection( IToastService toastService, IExpiringTokenService expiringTokenService, IDesktopHubSessionCache desktopSessionCache, - IServiceHubSessionCache serviceSessionCache, + IAgentHubSessionCache agentSessionCache, ILogger logger) { _dataService = dataService; @@ -93,7 +107,7 @@ public CircuitConnection( _toastService = toastService; _expiringTokenService = expiringTokenService; _desktopSessionCache = desktopSessionCache; - _serviceSessionCache = serviceSessionCache; + _agentSessionCache = agentSessionCache; _logger = logger; } @@ -101,7 +115,7 @@ public CircuitConnection( public event EventHandler MessageReceived; public string ConnectionId { get; set; } - public RemotelyUser User { get; set; } + public RemotelyUser User { get; internal set; } public Task DeleteRemoteLogs(string deviceId) @@ -209,7 +223,7 @@ public Task ReinstallAgents(string[] deviceIDs) public async Task> RemoteControl(string deviceId, bool viewOnly) { - if (!_serviceSessionCache.TryGetByDeviceId(deviceId, out var targetDevice)) + if (!_agentSessionCache.TryGetByDeviceId(deviceId, out var targetDevice)) { MessageReceived?.Invoke(this, new CircuitEvent(CircuitEventName.DisplayMessage, "The selected device is not online.", @@ -243,7 +257,7 @@ public async Task> RemoteControl(string deviceId, return Result.Fail("Max number of concurrent sessions reached."); } - if (!_serviceSessionCache.TryGetConnectionId(targetDevice.ID, out var serviceConnectionId)) + if (!_agentSessionCache.TryGetConnectionId(targetDevice.ID, out var serviceConnectionId)) { MessageReceived?.Invoke(this, new CircuitEvent(CircuitEventName.DisplayMessage, "Service connection not found.", @@ -303,7 +317,7 @@ public async Task RunScript(IEnumerable deviceIds, Guid savedScriptId, i var authToken = _expiringTokenService.GetToken(Time.Now.AddMinutes(AppConstants.ScriptRunExpirationMinutes)); - var connectionIds = _serviceSessionCache.GetConnectionIdsByDeviceIds(deviceIds).ToArray(); + var connectionIds = _agentSessionCache.GetConnectionIdsByDeviceIds(deviceIds).ToArray(); if (connectionIds.Any()) { @@ -319,8 +333,8 @@ public Task SendChat(string message, string deviceId) return Task.CompletedTask; } - if (!_serviceSessionCache.TryGetByDeviceId(deviceId, out var device) || - !_serviceSessionCache.TryGetConnectionId(deviceId, out var connectionId)) + if (!_agentSessionCache.TryGetByDeviceId(deviceId, out var device) || + !_agentSessionCache.TryGetConnectionId(deviceId, out var connectionId)) { _toastService.ShowToast("Device not found."); return Task.CompletedTask; @@ -345,7 +359,7 @@ public Task SendChat(string message, string deviceId) public async Task TransferFileFromBrowserToAgent(string deviceId, string transferId, string[] fileIds) { - if (!_serviceSessionCache.TryGetConnectionId(deviceId, out var connectionId)) + if (!_agentSessionCache.TryGetConnectionId(deviceId, out var connectionId)) { return false; } @@ -431,6 +445,89 @@ public Task UploadFiles(List fileIDs, string transferID, string[] device return Task.CompletedTask; } + public async Task WakeDevice(Device device) + { + try + { + if (!_dataService.DoesUserHaveAccessToDevice(device.ID, User.Id)) + { + return Result.Fail("Unauthorized.") ; + } + + var availableDevices = _agentSessionCache + .GetAllDevices() + .Where(x => + x.OrganizationID == User.OrganizationID && + (x.DeviceGroupID == device.DeviceGroupID || x.PublicIP == device.PublicIP)) + .ToArray(); + + await SendWakeCommand(device, availableDevices); + + return Result.Ok(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error waking device {deviceId}.", device.ID); + return Result.Fail(ex); + } + } + + public async Task WakeDevices(Device[] devices) + { + try + { + var deviceIds = devices.Select(x => x.ID).ToArray(); + var filteredIds = _dataService.FilterDeviceIDsByUserPermission(deviceIds, User); + var filteredDevices = devices.Where(x => filteredIds.Contains(x.ID)).ToArray(); + + var availableDevices = _agentSessionCache + .GetAllDevices() + .Where(x => x.OrganizationID == User.OrganizationID); + + var devicesByGroupId = new ConcurrentDictionary>(); + var devicesByPublicIp = new ConcurrentDictionary>(); + + foreach (var device in availableDevices) + { + if (!string.IsNullOrWhiteSpace(device.DeviceGroupID)) + { + var group = devicesByGroupId.GetOrAdd(device.DeviceGroupID, key => new()); + group.Add(device); + // We only need the device in one group. + break; + } + + if (!string.IsNullOrWhiteSpace(device.PublicIP)) + { + var group = devicesByPublicIp.GetOrAdd(device.PublicIP, key => new()); + group.Add(device); + } + } + + foreach (var deviceToWake in filteredDevices) + { + if (!string.IsNullOrWhiteSpace(deviceToWake.DeviceGroupID) && + devicesByGroupId.TryGetValue(deviceToWake.DeviceGroupID, out var groupList)) + { + await SendWakeCommand(deviceToWake, groupList); + } + + if (!string.IsNullOrWhiteSpace(deviceToWake.PublicIP) && + devicesByPublicIp.TryGetValue(deviceToWake.PublicIP, out var ipList)) + { + await SendWakeCommand(deviceToWake, ipList); + } + + } + return Result.Ok(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while waking devices."); + return Result.Fail(ex); + } + } + private (bool canAccess, string connectionId) CanAccessDevice(string deviceId) { if (string.IsNullOrWhiteSpace(deviceId)) @@ -438,13 +535,13 @@ public Task UploadFiles(List fileIDs, string transferID, string[] device return (false, string.Empty); } - if (!_serviceSessionCache.TryGetByDeviceId(deviceId, out var device) || + if (!_agentSessionCache.TryGetByDeviceId(deviceId, out var device) || !_dataService.DoesUserHaveAccessToDevice(device.ID, User) || - !_serviceSessionCache.TryGetConnectionId(device.ID, out var connectionId)) + !_agentSessionCache.TryGetConnectionId(device.ID, out var connectionId)) { return (false, string.Empty); } - + return (true, connectionId); } @@ -454,7 +551,7 @@ private IEnumerable GetActiveConnectionsForUserOrg(IEnumerable d foreach (var deviceId in deviceIds) { - if (!_serviceSessionCache.TryGetByDeviceId(deviceId, out var device)) + if (!_agentSessionCache.TryGetByDeviceId(deviceId, out var device)) { continue; } @@ -464,7 +561,7 @@ private IEnumerable GetActiveConnectionsForUserOrg(IEnumerable d continue; } - if (_serviceSessionCache.TryGetConnectionId(device.ID, out var connectionId)) + if (_agentSessionCache.TryGetConnectionId(device.ID, out var connectionId)) { yield return connectionId; } @@ -488,5 +585,28 @@ private void ProcessMessages() } } } + + private async Task SendWakeCommand(Device deviceToWake, IEnumerable peerDevices) + { + foreach (var peerDevice in peerDevices) + { + foreach (var mac in deviceToWake.MacAddresses ?? Array.Empty()) + { + if (_agentSessionCache.TryGetConnectionId(peerDevice.ID, out var connectionId)) + { + _logger.LogInformation( + "Sending wake command for device {deviceName} ({deviceId}) to " + + "peer device {peerDeviceName} ({peerDeviceId}). " + + "Sender: {username}.", + deviceToWake.DeviceName, + deviceToWake.ID, + peerDevice.DeviceName, + peerDevice.ID, + User.UserName); + await _agentHubContext.Clients.Client(connectionId).SendAsync("WakeDevice", mac); + } + } + } + } } } diff --git a/Server/Migrations/PostgreSql/20230622144413_Add_Agent_MacAddress.Designer.cs b/Server/Migrations/PostgreSql/20230622144413_Add_Agent_MacAddress.Designer.cs new file mode 100644 index 000000000..1e26335bd --- /dev/null +++ b/Server/Migrations/PostgreSql/20230622144413_Add_Agent_MacAddress.Designer.cs @@ -0,0 +1,1180 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Remotely.Server.Data; + +#nullable disable + +namespace Remotely.Server.Migrations.PostgreSql +{ + [DbContext(typeof(PostgreSqlDbContext))] + [Migration("20230622144413_Add_Agent_MacAddress")] + partial class Add_Agent_MacAddress + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("DeviceGroupRemotelyUser", b => + { + b.Property("DeviceGroupsID") + .HasColumnType("text"); + + b.Property("UsersId") + .HasColumnType("text"); + + b.HasKey("DeviceGroupsID", "UsersId"); + + b.HasIndex("UsersId"); + + b.ToTable("DeviceGroupRemotelyUser"); + }); + + modelBuilder.Entity("DeviceGroupScriptSchedule", b => + { + b.Property("DeviceGroupsID") + .HasColumnType("text"); + + b.Property("ScriptSchedulesId") + .HasColumnType("integer"); + + b.HasKey("DeviceGroupsID", "ScriptSchedulesId"); + + b.HasIndex("ScriptSchedulesId"); + + b.ToTable("DeviceGroupScriptSchedule"); + }); + + modelBuilder.Entity("DeviceScriptRun", b => + { + b.Property("DevicesID") + .HasColumnType("text"); + + b.Property("ScriptRunsId") + .HasColumnType("integer"); + + b.HasKey("DevicesID", "ScriptRunsId"); + + b.HasIndex("ScriptRunsId"); + + b.ToTable("DeviceScriptRun"); + }); + + modelBuilder.Entity("DeviceScriptRun1", b => + { + b.Property("DevicesCompletedID") + .HasColumnType("text"); + + b.Property("ScriptRunsCompletedId") + .HasColumnType("integer"); + + b.HasKey("DevicesCompletedID", "ScriptRunsCompletedId"); + + b.HasIndex("ScriptRunsCompletedId"); + + b.ToTable("DeviceScriptRun1"); + }); + + modelBuilder.Entity("DeviceScriptSchedule", b => + { + b.Property("DevicesID") + .HasColumnType("text"); + + b.Property("ScriptSchedulesId") + .HasColumnType("integer"); + + b.HasKey("DevicesID", "ScriptSchedulesId"); + + b.HasIndex("ScriptSchedulesId"); + + b.ToTable("DeviceScriptSchedule"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Discriminator") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("RemotelyUsers", (string)null); + + b.HasDiscriminator("Discriminator").HasValue("IdentityUser"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Remotely.Shared.Models.Alert", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Details") + .HasColumnType("text"); + + b.Property("DeviceID") + .HasColumnType("text"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("OrganizationID") + .HasColumnType("text"); + + b.Property("UserID") + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("DeviceID"); + + b.HasIndex("OrganizationID"); + + b.HasIndex("UserID"); + + b.ToTable("Alerts"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ApiToken", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("LastUsed") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("OrganizationID") + .HasColumnType("text"); + + b.Property("Secret") + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("OrganizationID"); + + b.ToTable("ApiTokens"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.BrandingInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ButtonForegroundBlue") + .HasColumnType("smallint"); + + b.Property("ButtonForegroundGreen") + .HasColumnType("smallint"); + + b.Property("ButtonForegroundRed") + .HasColumnType("smallint"); + + b.Property("Icon") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("OrganizationId") + .HasColumnType("text"); + + b.Property("Product") + .IsRequired() + .HasMaxLength(25) + .HasColumnType("character varying(25)"); + + b.Property("TitleBackgroundBlue") + .HasColumnType("smallint"); + + b.Property("TitleBackgroundGreen") + .HasColumnType("smallint"); + + b.Property("TitleBackgroundRed") + .HasColumnType("smallint"); + + b.Property("TitleForegroundBlue") + .HasColumnType("smallint"); + + b.Property("TitleForegroundGreen") + .HasColumnType("smallint"); + + b.Property("TitleForegroundRed") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("BrandingInfos"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.Device", b => + { + b.Property("ID") + .HasColumnType("text"); + + b.Property("AgentVersion") + .HasColumnType("text"); + + b.Property("Alias") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CpuUtilization") + .HasColumnType("double precision"); + + b.Property("CurrentUser") + .HasColumnType("text"); + + b.Property("DeviceGroupID") + .HasColumnType("text"); + + b.Property("DeviceName") + .HasColumnType("text"); + + b.Property("Drives") + .HasColumnType("text"); + + b.Property("Is64Bit") + .HasColumnType("boolean"); + + b.Property("IsOnline") + .HasColumnType("boolean"); + + b.Property("LastOnline") + .HasColumnType("timestamp with time zone"); + + b.Property("MacAddresses") + .HasColumnType("text"); + + b.Property("Notes") + .HasMaxLength(5000) + .HasColumnType("character varying(5000)"); + + b.Property("OSArchitecture") + .HasColumnType("integer"); + + b.Property("OSDescription") + .HasColumnType("text"); + + b.Property("OrganizationID") + .HasColumnType("text"); + + b.Property("Platform") + .HasColumnType("text"); + + b.Property("ProcessorCount") + .HasColumnType("integer"); + + b.Property("PublicIP") + .HasColumnType("text"); + + b.Property("ServerVerificationToken") + .HasColumnType("text"); + + b.Property("Tags") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TotalMemory") + .HasColumnType("double precision"); + + b.Property("TotalStorage") + .HasColumnType("double precision"); + + b.Property("UsedMemory") + .HasColumnType("double precision"); + + b.Property("UsedStorage") + .HasColumnType("double precision"); + + b.HasKey("ID"); + + b.HasIndex("DeviceGroupID"); + + b.HasIndex("DeviceName"); + + b.HasIndex("OrganizationID"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.DeviceGroup", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("OrganizationID") + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("OrganizationID"); + + b.ToTable("DeviceGroups"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.InviteLink", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("DateSent") + .HasColumnType("timestamp with time zone"); + + b.Property("InvitedUser") + .HasColumnType("text"); + + b.Property("IsAdmin") + .HasColumnType("boolean"); + + b.Property("OrganizationID") + .HasColumnType("text"); + + b.Property("ResetUrl") + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("OrganizationID"); + + b.ToTable("InviteLinks"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.Organization", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("IsDefaultOrganization") + .HasColumnType("boolean"); + + b.Property("OrganizationName") + .HasMaxLength(25) + .HasColumnType("character varying(25)"); + + b.HasKey("ID"); + + b.ToTable("Organizations"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.SavedScript", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatorId") + .HasColumnType("text"); + + b.Property("FolderPath") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("GenerateAlertOnError") + .HasColumnType("boolean"); + + b.Property("IsPublic") + .HasColumnType("boolean"); + + b.Property("IsQuickScript") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OrganizationID") + .HasColumnType("text"); + + b.Property("SendEmailOnError") + .HasColumnType("boolean"); + + b.Property("SendErrorEmailTo") + .HasColumnType("text"); + + b.Property("Shell") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CreatorId"); + + b.HasIndex("OrganizationID"); + + b.ToTable("SavedScripts"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ScriptResult", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("DeviceID") + .HasColumnType("text"); + + b.Property("ErrorOutput") + .HasColumnType("text"); + + b.Property("HadErrors") + .HasColumnType("boolean"); + + b.Property("InputType") + .HasColumnType("integer"); + + b.Property("OrganizationID") + .HasColumnType("text"); + + b.Property("RunTime") + .HasColumnType("interval"); + + b.Property("SavedScriptId") + .HasColumnType("uuid"); + + b.Property("ScheduleId") + .HasColumnType("integer"); + + b.Property("ScriptInput") + .HasColumnType("text"); + + b.Property("ScriptRunId") + .HasColumnType("integer"); + + b.Property("SenderConnectionID") + .HasColumnType("text"); + + b.Property("SenderUserName") + .HasColumnType("text"); + + b.Property("Shell") + .HasColumnType("integer"); + + b.Property("StandardOutput") + .HasColumnType("text"); + + b.Property("TimeStamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ID"); + + b.HasIndex("DeviceID"); + + b.HasIndex("OrganizationID"); + + b.HasIndex("ScheduleId"); + + b.HasIndex("ScriptRunId"); + + b.ToTable("ScriptResults"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ScriptRun", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Initiator") + .HasColumnType("text"); + + b.Property("InputType") + .HasColumnType("integer"); + + b.Property("OrganizationID") + .HasColumnType("text"); + + b.Property("RunAt") + .HasColumnType("timestamp with time zone"); + + b.Property("RunOnNextConnect") + .HasColumnType("boolean"); + + b.Property("SavedScriptId") + .HasColumnType("uuid"); + + b.Property("ScheduleId") + .HasColumnType("integer"); + + b.Property("ScriptScheduleId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationID"); + + b.HasIndex("ScriptScheduleId"); + + b.ToTable("ScriptRuns"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ScriptSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorId") + .HasColumnType("text"); + + b.Property("Interval") + .HasColumnType("integer"); + + b.Property("LastRun") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("NextRun") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationID") + .HasColumnType("text"); + + b.Property("RunOnNextConnect") + .HasColumnType("boolean"); + + b.Property("SavedScriptId") + .HasColumnType("uuid"); + + b.Property("StartAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatorId"); + + b.HasIndex("OrganizationID"); + + b.ToTable("ScriptSchedules"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.SharedFile", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ContentType") + .HasColumnType("text"); + + b.Property("FileContents") + .HasColumnType("bytea"); + + b.Property("FileName") + .HasColumnType("text"); + + b.Property("OrganizationID") + .HasColumnType("text"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("ID"); + + b.HasIndex("OrganizationID"); + + b.ToTable("SharedFiles"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.RemotelyUser", b => + { + b.HasBaseType("Microsoft.AspNetCore.Identity.IdentityUser"); + + b.Property("IsAdministrator") + .HasColumnType("boolean"); + + b.Property("IsServerAdmin") + .HasColumnType("boolean"); + + b.Property("OrganizationID") + .HasColumnType("text"); + + b.Property("TempPassword") + .HasColumnType("text"); + + b.Property("UserOptions") + .HasColumnType("text"); + + b.HasIndex("OrganizationID"); + + b.HasIndex("UserName"); + + b.HasDiscriminator().HasValue("RemotelyUser"); + }); + + modelBuilder.Entity("DeviceGroupRemotelyUser", b => + { + b.HasOne("Remotely.Shared.Models.DeviceGroup", null) + .WithMany() + .HasForeignKey("DeviceGroupsID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Models.RemotelyUser", null) + .WithMany() + .HasForeignKey("UsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("DeviceGroupScriptSchedule", b => + { + b.HasOne("Remotely.Shared.Models.DeviceGroup", null) + .WithMany() + .HasForeignKey("DeviceGroupsID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Models.ScriptSchedule", null) + .WithMany() + .HasForeignKey("ScriptSchedulesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("DeviceScriptRun", b => + { + b.HasOne("Remotely.Shared.Models.Device", null) + .WithMany() + .HasForeignKey("DevicesID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Models.ScriptRun", null) + .WithMany() + .HasForeignKey("ScriptRunsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("DeviceScriptRun1", b => + { + b.HasOne("Remotely.Shared.Models.Device", null) + .WithMany() + .HasForeignKey("DevicesCompletedID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Models.ScriptRun", null) + .WithMany() + .HasForeignKey("ScriptRunsCompletedId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("DeviceScriptSchedule", b => + { + b.HasOne("Remotely.Shared.Models.Device", null) + .WithMany() + .HasForeignKey("DevicesID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Models.ScriptSchedule", null) + .WithMany() + .HasForeignKey("ScriptSchedulesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Remotely.Shared.Models.Alert", b => + { + b.HasOne("Remotely.Shared.Models.Device", "Device") + .WithMany("Alerts") + .HasForeignKey("DeviceID"); + + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("Alerts") + .HasForeignKey("OrganizationID"); + + b.HasOne("Remotely.Shared.Models.RemotelyUser", "User") + .WithMany("Alerts") + .HasForeignKey("UserID"); + + b.Navigation("Device"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ApiToken", b => + { + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("ApiTokens") + .HasForeignKey("OrganizationID"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.BrandingInfo", b => + { + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithOne("BrandingInfo") + .HasForeignKey("Remotely.Shared.Models.BrandingInfo", "OrganizationId"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.Device", b => + { + b.HasOne("Remotely.Shared.Models.DeviceGroup", "DeviceGroup") + .WithMany("Devices") + .HasForeignKey("DeviceGroupID"); + + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("Devices") + .HasForeignKey("OrganizationID"); + + b.Navigation("DeviceGroup"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.DeviceGroup", b => + { + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("DeviceGroups") + .HasForeignKey("OrganizationID"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.InviteLink", b => + { + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("InviteLinks") + .HasForeignKey("OrganizationID"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.SavedScript", b => + { + b.HasOne("Remotely.Shared.Models.RemotelyUser", "Creator") + .WithMany("SavedScripts") + .HasForeignKey("CreatorId"); + + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("SavedScripts") + .HasForeignKey("OrganizationID"); + + b.Navigation("Creator"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ScriptResult", b => + { + b.HasOne("Remotely.Shared.Models.Device", "Device") + .WithMany("ScriptResults") + .HasForeignKey("DeviceID"); + + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("ScriptResults") + .HasForeignKey("OrganizationID"); + + b.HasOne("Remotely.Shared.Models.ScriptSchedule", "Schedule") + .WithMany() + .HasForeignKey("ScheduleId"); + + b.HasOne("Remotely.Shared.Models.ScriptRun", null) + .WithMany("Results") + .HasForeignKey("ScriptRunId"); + + b.Navigation("Device"); + + b.Navigation("Organization"); + + b.Navigation("Schedule"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ScriptRun", b => + { + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("ScriptRuns") + .HasForeignKey("OrganizationID"); + + b.HasOne("Remotely.Shared.Models.ScriptSchedule", null) + .WithMany("ScriptRuns") + .HasForeignKey("ScriptScheduleId"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ScriptSchedule", b => + { + b.HasOne("Remotely.Shared.Models.RemotelyUser", "Creator") + .WithMany("ScriptSchedules") + .HasForeignKey("CreatorId"); + + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("ScriptSchedules") + .HasForeignKey("OrganizationID"); + + b.Navigation("Creator"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.SharedFile", b => + { + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("SharedFiles") + .HasForeignKey("OrganizationID"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.RemotelyUser", b => + { + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("RemotelyUsers") + .HasForeignKey("OrganizationID"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.Device", b => + { + b.Navigation("Alerts"); + + b.Navigation("ScriptResults"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.DeviceGroup", b => + { + b.Navigation("Devices"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.Organization", b => + { + b.Navigation("Alerts"); + + b.Navigation("ApiTokens"); + + b.Navigation("BrandingInfo"); + + b.Navigation("DeviceGroups"); + + b.Navigation("Devices"); + + b.Navigation("InviteLinks"); + + b.Navigation("RemotelyUsers"); + + b.Navigation("SavedScripts"); + + b.Navigation("ScriptResults"); + + b.Navigation("ScriptRuns"); + + b.Navigation("ScriptSchedules"); + + b.Navigation("SharedFiles"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ScriptRun", b => + { + b.Navigation("Results"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ScriptSchedule", b => + { + b.Navigation("ScriptRuns"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.RemotelyUser", b => + { + b.Navigation("Alerts"); + + b.Navigation("SavedScripts"); + + b.Navigation("ScriptSchedules"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/PostgreSql/20230622144413_Add_Agent_MacAddress.cs b/Server/Migrations/PostgreSql/20230622144413_Add_Agent_MacAddress.cs new file mode 100644 index 000000000..d8203eaeb --- /dev/null +++ b/Server/Migrations/PostgreSql/20230622144413_Add_Agent_MacAddress.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Remotely.Server.Migrations.PostgreSql +{ + /// + public partial class Add_Agent_MacAddress : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "MacAddresses", + table: "Devices", + type: "text", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "MacAddresses", + table: "Devices"); + } + } +} diff --git a/Server/Migrations/PostgreSql/PostgreSqlDbContextModelSnapshot.cs b/Server/Migrations/PostgreSql/PostgreSqlDbContextModelSnapshot.cs index 5343ee4aa..fd996a8cf 100644 --- a/Server/Migrations/PostgreSql/PostgreSqlDbContextModelSnapshot.cs +++ b/Server/Migrations/PostgreSql/PostgreSqlDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("ProductVersion", "7.0.7") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -451,6 +451,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("LastOnline") .HasColumnType("timestamp with time zone"); + b.Property("MacAddresses") + .HasColumnType("text"); + b.Property("Notes") .HasMaxLength(5000) .HasColumnType("character varying(5000)"); diff --git a/Server/Migrations/SqlServer/20230622144404_Add_Agent_MacAddress.Designer.cs b/Server/Migrations/SqlServer/20230622144404_Add_Agent_MacAddress.Designer.cs new file mode 100644 index 000000000..02b6499ec --- /dev/null +++ b/Server/Migrations/SqlServer/20230622144404_Add_Agent_MacAddress.Designer.cs @@ -0,0 +1,1183 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Remotely.Server.Data; + +#nullable disable + +namespace Remotely.Server.Migrations.SqlServer +{ + [DbContext(typeof(SqlServerDbContext))] + [Migration("20230622144404_Add_Agent_MacAddress")] + partial class Add_Agent_MacAddress + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("DeviceGroupRemotelyUser", b => + { + b.Property("DeviceGroupsID") + .HasColumnType("nvarchar(450)"); + + b.Property("UsersId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("DeviceGroupsID", "UsersId"); + + b.HasIndex("UsersId"); + + b.ToTable("DeviceGroupRemotelyUser"); + }); + + modelBuilder.Entity("DeviceGroupScriptSchedule", b => + { + b.Property("DeviceGroupsID") + .HasColumnType("nvarchar(450)"); + + b.Property("ScriptSchedulesId") + .HasColumnType("int"); + + b.HasKey("DeviceGroupsID", "ScriptSchedulesId"); + + b.HasIndex("ScriptSchedulesId"); + + b.ToTable("DeviceGroupScriptSchedule"); + }); + + modelBuilder.Entity("DeviceScriptRun", b => + { + b.Property("DevicesID") + .HasColumnType("nvarchar(450)"); + + b.Property("ScriptRunsId") + .HasColumnType("int"); + + b.HasKey("DevicesID", "ScriptRunsId"); + + b.HasIndex("ScriptRunsId"); + + b.ToTable("DeviceScriptRun"); + }); + + modelBuilder.Entity("DeviceScriptRun1", b => + { + b.Property("DevicesCompletedID") + .HasColumnType("nvarchar(450)"); + + b.Property("ScriptRunsCompletedId") + .HasColumnType("int"); + + b.HasKey("DevicesCompletedID", "ScriptRunsCompletedId"); + + b.HasIndex("ScriptRunsCompletedId"); + + b.ToTable("DeviceScriptRun1"); + }); + + modelBuilder.Entity("DeviceScriptSchedule", b => + { + b.Property("DevicesID") + .HasColumnType("nvarchar(450)"); + + b.Property("ScriptSchedulesId") + .HasColumnType("int"); + + b.HasKey("DevicesID", "ScriptSchedulesId"); + + b.HasIndex("ScriptSchedulesId"); + + b.ToTable("DeviceScriptSchedule"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("nvarchar(450)"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Discriminator") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("RemotelyUsers", (string)null); + + b.HasDiscriminator("Discriminator").HasValue("IdentityUser"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("RoleId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("nvarchar(450)"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Remotely.Shared.Models.Alert", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset"); + + b.Property("Details") + .HasColumnType("nvarchar(max)"); + + b.Property("DeviceID") + .HasColumnType("nvarchar(450)"); + + b.Property("Message") + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizationID") + .HasColumnType("nvarchar(450)"); + + b.Property("UserID") + .HasColumnType("nvarchar(450)"); + + b.HasKey("ID"); + + b.HasIndex("DeviceID"); + + b.HasIndex("OrganizationID"); + + b.HasIndex("UserID"); + + b.ToTable("Alerts"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ApiToken", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("LastUsed") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OrganizationID") + .HasColumnType("nvarchar(450)"); + + b.Property("Secret") + .HasColumnType("nvarchar(max)"); + + b.HasKey("ID"); + + b.HasIndex("OrganizationID"); + + b.ToTable("ApiTokens"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.BrandingInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ButtonForegroundBlue") + .HasColumnType("tinyint"); + + b.Property("ButtonForegroundGreen") + .HasColumnType("tinyint"); + + b.Property("ButtonForegroundRed") + .HasColumnType("tinyint"); + + b.Property("Icon") + .IsRequired() + .HasColumnType("varbinary(max)"); + + b.Property("OrganizationId") + .HasColumnType("nvarchar(450)"); + + b.Property("Product") + .IsRequired() + .HasMaxLength(25) + .HasColumnType("nvarchar(25)"); + + b.Property("TitleBackgroundBlue") + .HasColumnType("tinyint"); + + b.Property("TitleBackgroundGreen") + .HasColumnType("tinyint"); + + b.Property("TitleBackgroundRed") + .HasColumnType("tinyint"); + + b.Property("TitleForegroundBlue") + .HasColumnType("tinyint"); + + b.Property("TitleForegroundGreen") + .HasColumnType("tinyint"); + + b.Property("TitleForegroundRed") + .HasColumnType("tinyint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique() + .HasFilter("[OrganizationId] IS NOT NULL"); + + b.ToTable("BrandingInfos"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.Device", b => + { + b.Property("ID") + .HasColumnType("nvarchar(450)"); + + b.Property("AgentVersion") + .HasColumnType("nvarchar(max)"); + + b.Property("Alias") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CpuUtilization") + .HasColumnType("float"); + + b.Property("CurrentUser") + .HasColumnType("nvarchar(max)"); + + b.Property("DeviceGroupID") + .HasColumnType("nvarchar(450)"); + + b.Property("DeviceName") + .HasColumnType("nvarchar(450)"); + + b.Property("Drives") + .HasColumnType("nvarchar(max)"); + + b.Property("Is64Bit") + .HasColumnType("bit"); + + b.Property("IsOnline") + .HasColumnType("bit"); + + b.Property("LastOnline") + .HasColumnType("datetimeoffset"); + + b.Property("MacAddresses") + .HasColumnType("nvarchar(max)"); + + b.Property("Notes") + .HasMaxLength(5000) + .HasColumnType("nvarchar(max)"); + + b.Property("OSArchitecture") + .HasColumnType("int"); + + b.Property("OSDescription") + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizationID") + .HasColumnType("nvarchar(450)"); + + b.Property("Platform") + .HasColumnType("nvarchar(max)"); + + b.Property("ProcessorCount") + .HasColumnType("int"); + + b.Property("PublicIP") + .HasColumnType("nvarchar(max)"); + + b.Property("ServerVerificationToken") + .HasColumnType("nvarchar(max)"); + + b.Property("Tags") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("TotalMemory") + .HasColumnType("float"); + + b.Property("TotalStorage") + .HasColumnType("float"); + + b.Property("UsedMemory") + .HasColumnType("float"); + + b.Property("UsedStorage") + .HasColumnType("float"); + + b.HasKey("ID"); + + b.HasIndex("DeviceGroupID"); + + b.HasIndex("DeviceName"); + + b.HasIndex("OrganizationID"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.DeviceGroup", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OrganizationID") + .HasColumnType("nvarchar(450)"); + + b.HasKey("ID"); + + b.HasIndex("OrganizationID"); + + b.ToTable("DeviceGroups"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.InviteLink", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("DateSent") + .HasColumnType("datetimeoffset"); + + b.Property("InvitedUser") + .HasColumnType("nvarchar(max)"); + + b.Property("IsAdmin") + .HasColumnType("bit"); + + b.Property("OrganizationID") + .HasColumnType("nvarchar(450)"); + + b.Property("ResetUrl") + .HasColumnType("nvarchar(max)"); + + b.HasKey("ID"); + + b.HasIndex("OrganizationID"); + + b.ToTable("InviteLinks"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.Organization", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("IsDefaultOrganization") + .HasColumnType("bit"); + + b.Property("OrganizationName") + .HasMaxLength(25) + .HasColumnType("nvarchar(25)"); + + b.HasKey("ID"); + + b.ToTable("Organizations"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.SavedScript", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatorId") + .HasColumnType("nvarchar(450)"); + + b.Property("FolderPath") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("GenerateAlertOnError") + .HasColumnType("bit"); + + b.Property("IsPublic") + .HasColumnType("bit"); + + b.Property("IsQuickScript") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("OrganizationID") + .HasColumnType("nvarchar(450)"); + + b.Property("SendEmailOnError") + .HasColumnType("bit"); + + b.Property("SendErrorEmailTo") + .HasColumnType("nvarchar(max)"); + + b.Property("Shell") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CreatorId"); + + b.HasIndex("OrganizationID"); + + b.ToTable("SavedScripts"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ScriptResult", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("DeviceID") + .HasColumnType("nvarchar(450)"); + + b.Property("ErrorOutput") + .HasColumnType("nvarchar(max)"); + + b.Property("HadErrors") + .HasColumnType("bit"); + + b.Property("InputType") + .HasColumnType("int"); + + b.Property("OrganizationID") + .HasColumnType("nvarchar(450)"); + + b.Property("RunTime") + .HasColumnType("time"); + + b.Property("SavedScriptId") + .HasColumnType("uniqueidentifier"); + + b.Property("ScheduleId") + .HasColumnType("int"); + + b.Property("ScriptInput") + .HasColumnType("nvarchar(max)"); + + b.Property("ScriptRunId") + .HasColumnType("int"); + + b.Property("SenderConnectionID") + .HasColumnType("nvarchar(max)"); + + b.Property("SenderUserName") + .HasColumnType("nvarchar(max)"); + + b.Property("Shell") + .HasColumnType("int"); + + b.Property("StandardOutput") + .HasColumnType("nvarchar(max)"); + + b.Property("TimeStamp") + .HasColumnType("datetimeoffset"); + + b.HasKey("ID"); + + b.HasIndex("DeviceID"); + + b.HasIndex("OrganizationID"); + + b.HasIndex("ScheduleId"); + + b.HasIndex("ScriptRunId"); + + b.ToTable("ScriptResults"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ScriptRun", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Initiator") + .HasColumnType("nvarchar(max)"); + + b.Property("InputType") + .HasColumnType("int"); + + b.Property("OrganizationID") + .HasColumnType("nvarchar(450)"); + + b.Property("RunAt") + .HasColumnType("datetimeoffset"); + + b.Property("RunOnNextConnect") + .HasColumnType("bit"); + + b.Property("SavedScriptId") + .HasColumnType("uniqueidentifier"); + + b.Property("ScheduleId") + .HasColumnType("int"); + + b.Property("ScriptScheduleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationID"); + + b.HasIndex("ScriptScheduleId"); + + b.ToTable("ScriptRuns"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ScriptSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatorId") + .HasColumnType("nvarchar(450)"); + + b.Property("Interval") + .HasColumnType("int"); + + b.Property("LastRun") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("NextRun") + .HasColumnType("datetimeoffset"); + + b.Property("OrganizationID") + .HasColumnType("nvarchar(450)"); + + b.Property("RunOnNextConnect") + .HasColumnType("bit"); + + b.Property("SavedScriptId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartAt") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.HasIndex("CreatorId"); + + b.HasIndex("OrganizationID"); + + b.ToTable("ScriptSchedules"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.SharedFile", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ContentType") + .HasColumnType("nvarchar(max)"); + + b.Property("FileContents") + .HasColumnType("varbinary(max)"); + + b.Property("FileName") + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizationID") + .HasColumnType("nvarchar(450)"); + + b.Property("Timestamp") + .HasColumnType("datetimeoffset"); + + b.HasKey("ID"); + + b.HasIndex("OrganizationID"); + + b.ToTable("SharedFiles"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.RemotelyUser", b => + { + b.HasBaseType("Microsoft.AspNetCore.Identity.IdentityUser"); + + b.Property("IsAdministrator") + .HasColumnType("bit"); + + b.Property("IsServerAdmin") + .HasColumnType("bit"); + + b.Property("OrganizationID") + .HasColumnType("nvarchar(450)"); + + b.Property("TempPassword") + .HasColumnType("nvarchar(max)"); + + b.Property("UserOptions") + .HasColumnType("nvarchar(max)"); + + b.HasIndex("OrganizationID"); + + b.HasIndex("UserName"); + + b.HasDiscriminator().HasValue("RemotelyUser"); + }); + + modelBuilder.Entity("DeviceGroupRemotelyUser", b => + { + b.HasOne("Remotely.Shared.Models.DeviceGroup", null) + .WithMany() + .HasForeignKey("DeviceGroupsID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Models.RemotelyUser", null) + .WithMany() + .HasForeignKey("UsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("DeviceGroupScriptSchedule", b => + { + b.HasOne("Remotely.Shared.Models.DeviceGroup", null) + .WithMany() + .HasForeignKey("DeviceGroupsID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Models.ScriptSchedule", null) + .WithMany() + .HasForeignKey("ScriptSchedulesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("DeviceScriptRun", b => + { + b.HasOne("Remotely.Shared.Models.Device", null) + .WithMany() + .HasForeignKey("DevicesID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Models.ScriptRun", null) + .WithMany() + .HasForeignKey("ScriptRunsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("DeviceScriptRun1", b => + { + b.HasOne("Remotely.Shared.Models.Device", null) + .WithMany() + .HasForeignKey("DevicesCompletedID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Models.ScriptRun", null) + .WithMany() + .HasForeignKey("ScriptRunsCompletedId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("DeviceScriptSchedule", b => + { + b.HasOne("Remotely.Shared.Models.Device", null) + .WithMany() + .HasForeignKey("DevicesID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Models.ScriptSchedule", null) + .WithMany() + .HasForeignKey("ScriptSchedulesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Remotely.Shared.Models.Alert", b => + { + b.HasOne("Remotely.Shared.Models.Device", "Device") + .WithMany("Alerts") + .HasForeignKey("DeviceID"); + + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("Alerts") + .HasForeignKey("OrganizationID"); + + b.HasOne("Remotely.Shared.Models.RemotelyUser", "User") + .WithMany("Alerts") + .HasForeignKey("UserID"); + + b.Navigation("Device"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ApiToken", b => + { + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("ApiTokens") + .HasForeignKey("OrganizationID"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.BrandingInfo", b => + { + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithOne("BrandingInfo") + .HasForeignKey("Remotely.Shared.Models.BrandingInfo", "OrganizationId"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.Device", b => + { + b.HasOne("Remotely.Shared.Models.DeviceGroup", "DeviceGroup") + .WithMany("Devices") + .HasForeignKey("DeviceGroupID"); + + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("Devices") + .HasForeignKey("OrganizationID"); + + b.Navigation("DeviceGroup"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.DeviceGroup", b => + { + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("DeviceGroups") + .HasForeignKey("OrganizationID"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.InviteLink", b => + { + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("InviteLinks") + .HasForeignKey("OrganizationID"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.SavedScript", b => + { + b.HasOne("Remotely.Shared.Models.RemotelyUser", "Creator") + .WithMany("SavedScripts") + .HasForeignKey("CreatorId"); + + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("SavedScripts") + .HasForeignKey("OrganizationID"); + + b.Navigation("Creator"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ScriptResult", b => + { + b.HasOne("Remotely.Shared.Models.Device", "Device") + .WithMany("ScriptResults") + .HasForeignKey("DeviceID"); + + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("ScriptResults") + .HasForeignKey("OrganizationID"); + + b.HasOne("Remotely.Shared.Models.ScriptSchedule", "Schedule") + .WithMany() + .HasForeignKey("ScheduleId"); + + b.HasOne("Remotely.Shared.Models.ScriptRun", null) + .WithMany("Results") + .HasForeignKey("ScriptRunId"); + + b.Navigation("Device"); + + b.Navigation("Organization"); + + b.Navigation("Schedule"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ScriptRun", b => + { + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("ScriptRuns") + .HasForeignKey("OrganizationID"); + + b.HasOne("Remotely.Shared.Models.ScriptSchedule", null) + .WithMany("ScriptRuns") + .HasForeignKey("ScriptScheduleId"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ScriptSchedule", b => + { + b.HasOne("Remotely.Shared.Models.RemotelyUser", "Creator") + .WithMany("ScriptSchedules") + .HasForeignKey("CreatorId"); + + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("ScriptSchedules") + .HasForeignKey("OrganizationID"); + + b.Navigation("Creator"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.SharedFile", b => + { + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("SharedFiles") + .HasForeignKey("OrganizationID"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.RemotelyUser", b => + { + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("RemotelyUsers") + .HasForeignKey("OrganizationID"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.Device", b => + { + b.Navigation("Alerts"); + + b.Navigation("ScriptResults"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.DeviceGroup", b => + { + b.Navigation("Devices"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.Organization", b => + { + b.Navigation("Alerts"); + + b.Navigation("ApiTokens"); + + b.Navigation("BrandingInfo"); + + b.Navigation("DeviceGroups"); + + b.Navigation("Devices"); + + b.Navigation("InviteLinks"); + + b.Navigation("RemotelyUsers"); + + b.Navigation("SavedScripts"); + + b.Navigation("ScriptResults"); + + b.Navigation("ScriptRuns"); + + b.Navigation("ScriptSchedules"); + + b.Navigation("SharedFiles"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ScriptRun", b => + { + b.Navigation("Results"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ScriptSchedule", b => + { + b.Navigation("ScriptRuns"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.RemotelyUser", b => + { + b.Navigation("Alerts"); + + b.Navigation("SavedScripts"); + + b.Navigation("ScriptSchedules"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/SqlServer/20230622144404_Add_Agent_MacAddress.cs b/Server/Migrations/SqlServer/20230622144404_Add_Agent_MacAddress.cs new file mode 100644 index 000000000..29391188c --- /dev/null +++ b/Server/Migrations/SqlServer/20230622144404_Add_Agent_MacAddress.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Remotely.Server.Migrations.SqlServer +{ + /// + public partial class Add_Agent_MacAddress : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "MacAddresses", + table: "Devices", + type: "nvarchar(max)", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "MacAddresses", + table: "Devices"); + } + } +} diff --git a/Server/Migrations/SqlServer/SqlServerDbContextModelSnapshot.cs b/Server/Migrations/SqlServer/SqlServerDbContextModelSnapshot.cs index fbcea8b31..24a0740bb 100644 --- a/Server/Migrations/SqlServer/SqlServerDbContextModelSnapshot.cs +++ b/Server/Migrations/SqlServer/SqlServerDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "7.0.5") + .HasAnnotation("ProductVersion", "7.0.7") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -454,6 +454,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("LastOnline") .HasColumnType("datetimeoffset"); + b.Property("MacAddresses") + .HasColumnType("nvarchar(max)"); + b.Property("Notes") .HasMaxLength(5000) .HasColumnType("nvarchar(max)"); diff --git a/Server/Migrations/Sqlite/20230622144355_Add_Agent_MacAddress.Designer.cs b/Server/Migrations/Sqlite/20230622144355_Add_Agent_MacAddress.Designer.cs new file mode 100644 index 000000000..ef45f7415 --- /dev/null +++ b/Server/Migrations/Sqlite/20230622144355_Add_Agent_MacAddress.Designer.cs @@ -0,0 +1,1176 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Remotely.Server.Data; + +#nullable disable + +namespace Remotely.Server.Migrations.Sqlite +{ + [DbContext(typeof(SqliteDbContext))] + [Migration("20230622144355_Add_Agent_MacAddress")] + partial class Add_Agent_MacAddress + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.7"); + + modelBuilder.Entity("DeviceGroupRemotelyUser", b => + { + b.Property("DeviceGroupsID") + .HasColumnType("TEXT"); + + b.Property("UsersId") + .HasColumnType("TEXT"); + + b.HasKey("DeviceGroupsID", "UsersId"); + + b.HasIndex("UsersId"); + + b.ToTable("DeviceGroupRemotelyUser"); + }); + + modelBuilder.Entity("DeviceGroupScriptSchedule", b => + { + b.Property("DeviceGroupsID") + .HasColumnType("TEXT"); + + b.Property("ScriptSchedulesId") + .HasColumnType("INTEGER"); + + b.HasKey("DeviceGroupsID", "ScriptSchedulesId"); + + b.HasIndex("ScriptSchedulesId"); + + b.ToTable("DeviceGroupScriptSchedule"); + }); + + modelBuilder.Entity("DeviceScriptRun", b => + { + b.Property("DevicesID") + .HasColumnType("TEXT"); + + b.Property("ScriptRunsId") + .HasColumnType("INTEGER"); + + b.HasKey("DevicesID", "ScriptRunsId"); + + b.HasIndex("ScriptRunsId"); + + b.ToTable("DeviceScriptRun"); + }); + + modelBuilder.Entity("DeviceScriptRun1", b => + { + b.Property("DevicesCompletedID") + .HasColumnType("TEXT"); + + b.Property("ScriptRunsCompletedId") + .HasColumnType("INTEGER"); + + b.HasKey("DevicesCompletedID", "ScriptRunsCompletedId"); + + b.HasIndex("ScriptRunsCompletedId"); + + b.ToTable("DeviceScriptRun1"); + }); + + modelBuilder.Entity("DeviceScriptSchedule", b => + { + b.Property("DevicesID") + .HasColumnType("TEXT"); + + b.Property("ScriptSchedulesId") + .HasColumnType("INTEGER"); + + b.HasKey("DevicesID", "ScriptSchedulesId"); + + b.HasIndex("ScriptSchedulesId"); + + b.ToTable("DeviceScriptSchedule"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Discriminator") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("RemotelyUsers", (string)null); + + b.HasDiscriminator("Discriminator").HasValue("IdentityUser"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("Remotely.Shared.Models.Alert", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("DeviceID") + .HasColumnType("TEXT"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("OrganizationID") + .HasColumnType("TEXT"); + + b.Property("UserID") + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("DeviceID"); + + b.HasIndex("OrganizationID"); + + b.HasIndex("UserID"); + + b.ToTable("Alerts"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ApiToken", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("LastUsed") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationID") + .HasColumnType("TEXT"); + + b.Property("Secret") + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("OrganizationID"); + + b.ToTable("ApiTokens"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.BrandingInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ButtonForegroundBlue") + .HasColumnType("INTEGER"); + + b.Property("ButtonForegroundGreen") + .HasColumnType("INTEGER"); + + b.Property("ButtonForegroundRed") + .HasColumnType("INTEGER"); + + b.Property("Icon") + .IsRequired() + .HasColumnType("BLOB"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Product") + .IsRequired() + .HasMaxLength(25) + .HasColumnType("TEXT"); + + b.Property("TitleBackgroundBlue") + .HasColumnType("INTEGER"); + + b.Property("TitleBackgroundGreen") + .HasColumnType("INTEGER"); + + b.Property("TitleBackgroundRed") + .HasColumnType("INTEGER"); + + b.Property("TitleForegroundBlue") + .HasColumnType("INTEGER"); + + b.Property("TitleForegroundGreen") + .HasColumnType("INTEGER"); + + b.Property("TitleForegroundRed") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("BrandingInfos"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.Device", b => + { + b.Property("ID") + .HasColumnType("TEXT"); + + b.Property("AgentVersion") + .HasColumnType("TEXT"); + + b.Property("Alias") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("CpuUtilization") + .HasColumnType("REAL"); + + b.Property("CurrentUser") + .HasColumnType("TEXT"); + + b.Property("DeviceGroupID") + .HasColumnType("TEXT"); + + b.Property("DeviceName") + .HasColumnType("TEXT"); + + b.Property("Drives") + .HasColumnType("TEXT"); + + b.Property("Is64Bit") + .HasColumnType("INTEGER"); + + b.Property("IsOnline") + .HasColumnType("INTEGER"); + + b.Property("LastOnline") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("MacAddresses") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(5000) + .HasColumnType("TEXT"); + + b.Property("OSArchitecture") + .HasColumnType("INTEGER"); + + b.Property("OSDescription") + .HasColumnType("TEXT"); + + b.Property("OrganizationID") + .HasColumnType("TEXT"); + + b.Property("Platform") + .HasColumnType("TEXT"); + + b.Property("ProcessorCount") + .HasColumnType("INTEGER"); + + b.Property("PublicIP") + .HasColumnType("TEXT"); + + b.Property("ServerVerificationToken") + .HasColumnType("TEXT"); + + b.Property("Tags") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("TotalMemory") + .HasColumnType("REAL"); + + b.Property("TotalStorage") + .HasColumnType("REAL"); + + b.Property("UsedMemory") + .HasColumnType("REAL"); + + b.Property("UsedStorage") + .HasColumnType("REAL"); + + b.HasKey("ID"); + + b.HasIndex("DeviceGroupID"); + + b.HasIndex("DeviceName"); + + b.HasIndex("OrganizationID"); + + b.ToTable("Devices"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.DeviceGroup", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationID") + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("OrganizationID"); + + b.ToTable("DeviceGroups"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.InviteLink", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("DateSent") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("InvitedUser") + .HasColumnType("TEXT"); + + b.Property("IsAdmin") + .HasColumnType("INTEGER"); + + b.Property("OrganizationID") + .HasColumnType("TEXT"); + + b.Property("ResetUrl") + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("OrganizationID"); + + b.ToTable("InviteLinks"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.Organization", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsDefaultOrganization") + .HasColumnType("INTEGER"); + + b.Property("OrganizationName") + .HasMaxLength(25) + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.ToTable("Organizations"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.SavedScript", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatorId") + .HasColumnType("TEXT"); + + b.Property("FolderPath") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("GenerateAlertOnError") + .HasColumnType("INTEGER"); + + b.Property("IsPublic") + .HasColumnType("INTEGER"); + + b.Property("IsQuickScript") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationID") + .HasColumnType("TEXT"); + + b.Property("SendEmailOnError") + .HasColumnType("INTEGER"); + + b.Property("SendErrorEmailTo") + .HasColumnType("TEXT"); + + b.Property("Shell") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CreatorId"); + + b.HasIndex("OrganizationID"); + + b.ToTable("SavedScripts"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ScriptResult", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("DeviceID") + .HasColumnType("TEXT"); + + b.Property("ErrorOutput") + .HasColumnType("TEXT"); + + b.Property("HadErrors") + .HasColumnType("INTEGER"); + + b.Property("InputType") + .HasColumnType("INTEGER"); + + b.Property("OrganizationID") + .HasColumnType("TEXT"); + + b.Property("RunTime") + .HasColumnType("TEXT"); + + b.Property("SavedScriptId") + .HasColumnType("TEXT"); + + b.Property("ScheduleId") + .HasColumnType("INTEGER"); + + b.Property("ScriptInput") + .HasColumnType("TEXT"); + + b.Property("ScriptRunId") + .HasColumnType("INTEGER"); + + b.Property("SenderConnectionID") + .HasColumnType("TEXT"); + + b.Property("SenderUserName") + .HasColumnType("TEXT"); + + b.Property("Shell") + .HasColumnType("INTEGER"); + + b.Property("StandardOutput") + .HasColumnType("TEXT"); + + b.Property("TimeStamp") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("DeviceID"); + + b.HasIndex("OrganizationID"); + + b.HasIndex("ScheduleId"); + + b.HasIndex("ScriptRunId"); + + b.ToTable("ScriptResults"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ScriptRun", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Initiator") + .HasColumnType("TEXT"); + + b.Property("InputType") + .HasColumnType("INTEGER"); + + b.Property("OrganizationID") + .HasColumnType("TEXT"); + + b.Property("RunAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RunOnNextConnect") + .HasColumnType("INTEGER"); + + b.Property("SavedScriptId") + .HasColumnType("TEXT"); + + b.Property("ScheduleId") + .HasColumnType("INTEGER"); + + b.Property("ScriptScheduleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationID"); + + b.HasIndex("ScriptScheduleId"); + + b.ToTable("ScriptRuns"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ScriptSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatorId") + .HasColumnType("TEXT"); + + b.Property("Interval") + .HasColumnType("INTEGER"); + + b.Property("LastRun") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("NextRun") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationID") + .HasColumnType("TEXT"); + + b.Property("RunOnNextConnect") + .HasColumnType("INTEGER"); + + b.Property("SavedScriptId") + .HasColumnType("TEXT"); + + b.Property("StartAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatorId"); + + b.HasIndex("OrganizationID"); + + b.ToTable("ScriptSchedules"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.SharedFile", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ContentType") + .HasColumnType("TEXT"); + + b.Property("FileContents") + .HasColumnType("BLOB"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("OrganizationID") + .HasColumnType("TEXT"); + + b.Property("Timestamp") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("OrganizationID"); + + b.ToTable("SharedFiles"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.RemotelyUser", b => + { + b.HasBaseType("Microsoft.AspNetCore.Identity.IdentityUser"); + + b.Property("IsAdministrator") + .HasColumnType("INTEGER"); + + b.Property("IsServerAdmin") + .HasColumnType("INTEGER"); + + b.Property("OrganizationID") + .HasColumnType("TEXT"); + + b.Property("TempPassword") + .HasColumnType("TEXT"); + + b.Property("UserOptions") + .HasColumnType("TEXT"); + + b.HasIndex("OrganizationID"); + + b.HasIndex("UserName"); + + b.HasDiscriminator().HasValue("RemotelyUser"); + }); + + modelBuilder.Entity("DeviceGroupRemotelyUser", b => + { + b.HasOne("Remotely.Shared.Models.DeviceGroup", null) + .WithMany() + .HasForeignKey("DeviceGroupsID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Models.RemotelyUser", null) + .WithMany() + .HasForeignKey("UsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("DeviceGroupScriptSchedule", b => + { + b.HasOne("Remotely.Shared.Models.DeviceGroup", null) + .WithMany() + .HasForeignKey("DeviceGroupsID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Models.ScriptSchedule", null) + .WithMany() + .HasForeignKey("ScriptSchedulesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("DeviceScriptRun", b => + { + b.HasOne("Remotely.Shared.Models.Device", null) + .WithMany() + .HasForeignKey("DevicesID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Models.ScriptRun", null) + .WithMany() + .HasForeignKey("ScriptRunsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("DeviceScriptRun1", b => + { + b.HasOne("Remotely.Shared.Models.Device", null) + .WithMany() + .HasForeignKey("DevicesCompletedID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Models.ScriptRun", null) + .WithMany() + .HasForeignKey("ScriptRunsCompletedId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("DeviceScriptSchedule", b => + { + b.HasOne("Remotely.Shared.Models.Device", null) + .WithMany() + .HasForeignKey("DevicesID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Models.ScriptSchedule", null) + .WithMany() + .HasForeignKey("ScriptSchedulesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Remotely.Shared.Models.Alert", b => + { + b.HasOne("Remotely.Shared.Models.Device", "Device") + .WithMany("Alerts") + .HasForeignKey("DeviceID"); + + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("Alerts") + .HasForeignKey("OrganizationID"); + + b.HasOne("Remotely.Shared.Models.RemotelyUser", "User") + .WithMany("Alerts") + .HasForeignKey("UserID"); + + b.Navigation("Device"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ApiToken", b => + { + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("ApiTokens") + .HasForeignKey("OrganizationID"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.BrandingInfo", b => + { + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithOne("BrandingInfo") + .HasForeignKey("Remotely.Shared.Models.BrandingInfo", "OrganizationId"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.Device", b => + { + b.HasOne("Remotely.Shared.Models.DeviceGroup", "DeviceGroup") + .WithMany("Devices") + .HasForeignKey("DeviceGroupID"); + + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("Devices") + .HasForeignKey("OrganizationID"); + + b.Navigation("DeviceGroup"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.DeviceGroup", b => + { + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("DeviceGroups") + .HasForeignKey("OrganizationID"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.InviteLink", b => + { + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("InviteLinks") + .HasForeignKey("OrganizationID"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.SavedScript", b => + { + b.HasOne("Remotely.Shared.Models.RemotelyUser", "Creator") + .WithMany("SavedScripts") + .HasForeignKey("CreatorId"); + + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("SavedScripts") + .HasForeignKey("OrganizationID"); + + b.Navigation("Creator"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ScriptResult", b => + { + b.HasOne("Remotely.Shared.Models.Device", "Device") + .WithMany("ScriptResults") + .HasForeignKey("DeviceID"); + + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("ScriptResults") + .HasForeignKey("OrganizationID"); + + b.HasOne("Remotely.Shared.Models.ScriptSchedule", "Schedule") + .WithMany() + .HasForeignKey("ScheduleId"); + + b.HasOne("Remotely.Shared.Models.ScriptRun", null) + .WithMany("Results") + .HasForeignKey("ScriptRunId"); + + b.Navigation("Device"); + + b.Navigation("Organization"); + + b.Navigation("Schedule"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ScriptRun", b => + { + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("ScriptRuns") + .HasForeignKey("OrganizationID"); + + b.HasOne("Remotely.Shared.Models.ScriptSchedule", null) + .WithMany("ScriptRuns") + .HasForeignKey("ScriptScheduleId"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ScriptSchedule", b => + { + b.HasOne("Remotely.Shared.Models.RemotelyUser", "Creator") + .WithMany("ScriptSchedules") + .HasForeignKey("CreatorId"); + + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("ScriptSchedules") + .HasForeignKey("OrganizationID"); + + b.Navigation("Creator"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.SharedFile", b => + { + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("SharedFiles") + .HasForeignKey("OrganizationID"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.RemotelyUser", b => + { + b.HasOne("Remotely.Shared.Models.Organization", "Organization") + .WithMany("RemotelyUsers") + .HasForeignKey("OrganizationID"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.Device", b => + { + b.Navigation("Alerts"); + + b.Navigation("ScriptResults"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.DeviceGroup", b => + { + b.Navigation("Devices"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.Organization", b => + { + b.Navigation("Alerts"); + + b.Navigation("ApiTokens"); + + b.Navigation("BrandingInfo"); + + b.Navigation("DeviceGroups"); + + b.Navigation("Devices"); + + b.Navigation("InviteLinks"); + + b.Navigation("RemotelyUsers"); + + b.Navigation("SavedScripts"); + + b.Navigation("ScriptResults"); + + b.Navigation("ScriptRuns"); + + b.Navigation("ScriptSchedules"); + + b.Navigation("SharedFiles"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ScriptRun", b => + { + b.Navigation("Results"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.ScriptSchedule", b => + { + b.Navigation("ScriptRuns"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.RemotelyUser", b => + { + b.Navigation("Alerts"); + + b.Navigation("SavedScripts"); + + b.Navigation("ScriptSchedules"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/Sqlite/20230622144355_Add_Agent_MacAddress.cs b/Server/Migrations/Sqlite/20230622144355_Add_Agent_MacAddress.cs new file mode 100644 index 000000000..da35976d7 --- /dev/null +++ b/Server/Migrations/Sqlite/20230622144355_Add_Agent_MacAddress.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Remotely.Server.Migrations.Sqlite +{ + /// + public partial class Add_Agent_MacAddress : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "MacAddresses", + table: "Devices", + type: "TEXT", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "MacAddresses", + table: "Devices"); + } + } +} diff --git a/Server/Migrations/Sqlite/SqliteDbContextModelSnapshot.cs b/Server/Migrations/Sqlite/SqliteDbContextModelSnapshot.cs index dda694e9d..3e9cb5ab2 100644 --- a/Server/Migrations/Sqlite/SqliteDbContextModelSnapshot.cs +++ b/Server/Migrations/Sqlite/SqliteDbContextModelSnapshot.cs @@ -15,7 +15,7 @@ partial class SqliteDbContextModelSnapshot : ModelSnapshot protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "7.0.5"); + modelBuilder.HasAnnotation("ProductVersion", "7.0.7"); modelBuilder.Entity("DeviceGroupRemotelyUser", b => { @@ -444,6 +444,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("TEXT"); + b.Property("MacAddresses") + .HasColumnType("TEXT"); + b.Property("Notes") .HasMaxLength(5000) .HasColumnType("TEXT"); diff --git a/Server/Pages/DeviceDetails.razor b/Server/Pages/DeviceDetails.razor index fc45dfe3d..b52a521cc 100644 --- a/Server/Pages/DeviceDetails.razor +++ b/Server/Pages/DeviceDetails.razor @@ -134,6 +134,27 @@ else
    +
    + +
    + +
    +
    +
    + +
    + ()))" /> +
    +
    @@ -148,9 +169,9 @@ else @foreach (var group in DataService.GetDeviceGroups(Username)) - { + { - } + }
    diff --git a/Server/Pages/ServerConfig.razor.cs b/Server/Pages/ServerConfig.razor.cs index 88d7a647e..a78ce72e3 100644 --- a/Server/Pages/ServerConfig.razor.cs +++ b/Server/Pages/ServerConfig.razor.cs @@ -162,7 +162,7 @@ public partial class ServerConfig : AuthComponentBase private ILogger Logger { get; set; } [Inject] - private IServiceHubSessionCache ServiceSessionCache { get; init; } + private IAgentHubSessionCache ServiceSessionCache { get; init; } private AppSettingsModel Input { get; } = new(); diff --git a/Server/Pages/ServerLogs.razor b/Server/Pages/ServerLogs.razor index f8f1c849e..0f4caa201 100644 --- a/Server/Pages/ServerLogs.razor +++ b/Server/Pages/ServerLogs.razor @@ -69,7 +69,7 @@ private LogLevel? _logLevel; private string _messageFilter = string.Empty; - private DateTimeOffset _fromDate = DateTimeOffset.Now.AddDays(-7); + private DateTimeOffset _fromDate = DateTimeOffset.Now.AddDays(-1); private DateTimeOffset _toDate = DateTimeOffset.Now; private DateTimeOffset FromDate diff --git a/Server/Program.cs b/Server/Program.cs index 0b97c74f8..7e68bb5f5 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -219,7 +219,7 @@ services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); -services.AddSingleton(WeakReferenceMessenger.Default); +services.AddScoped((services) => new WeakReferenceMessenger()); services.AddRemoteControlServer(config => { @@ -228,7 +228,7 @@ config.AddViewerPageDataProvider(); }); -services.AddSingleton(); +services.AddSingleton(); var app = builder.Build(); var appConfig = app.Services.GetRequiredService(); diff --git a/Server/Properties/AssemblyInfo.cs b/Server/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..d6663dea7 --- /dev/null +++ b/Server/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Remotely.Server.Tests")] \ No newline at end of file diff --git a/Server/Services/ServiceHubConnectionCache.cs b/Server/Services/AgentHubSessionCache.cs similarity index 95% rename from Server/Services/ServiceHubConnectionCache.cs rename to Server/Services/AgentHubSessionCache.cs index d610e3814..127a0c2a1 100644 --- a/Server/Services/ServiceHubConnectionCache.cs +++ b/Server/Services/AgentHubSessionCache.cs @@ -6,7 +6,7 @@ namespace Remotely.Server.Services { - public interface IServiceHubSessionCache + public interface IAgentHubSessionCache { void AddOrUpdateByConnectionId(string connectionId, Device device); ICollection GetAllDevices(); @@ -16,7 +16,7 @@ public interface IServiceHubSessionCache bool TryRemoveByConnectionId(string connectionId, out Device device); } - public class ServiceHubSessionCache : IServiceHubSessionCache + public class AgentHubSessionCache : IAgentHubSessionCache { private readonly ConcurrentDictionary _connectionIdToDeviceLookup = new(); diff --git a/Server/Services/DataService.cs b/Server/Services/DataService.cs index 0c02c0395..43d797273 100644 --- a/Server/Services/DataService.cs +++ b/Server/Services/DataService.cs @@ -10,6 +10,7 @@ using Remotely.Server.Data; using Remotely.Server.Models; using Remotely.Shared; +using Remotely.Shared.Dtos; using Remotely.Shared.Enums; using Remotely.Shared.Models; using Remotely.Shared.Utilities; @@ -27,10 +28,10 @@ public interface IDataService Task AddAlert(string deviceID, string organizationID, string alertMessage, string details = null); bool AddDeviceGroup(string orgID, DeviceGroup deviceGroup, out string deviceGroupID, out string errorMessage); - + Task AddDeviceToGroup(string deviceId, string groupId); InviteLink AddInvite(string orgID, InviteViewModel invite); - Task> AddOrUpdateDevice(Device device); + Task> AddOrUpdateDevice(DeviceClientDto device); Task AddOrUpdateSavedScript(SavedScript script, string userId); @@ -124,7 +125,11 @@ public interface IDataService int GetDeviceCount(RemotelyUser user); - Task GetDeviceGroup(string deviceGroupID); + Task GetDeviceGroup( + string deviceGroupID, + bool includeDevices = false, + bool includeUsers = false); + DeviceGroup[] GetDeviceGroups(string username); DeviceGroup[] GetDeviceGroupsForOrganization(string organizationId); @@ -301,6 +306,34 @@ public bool AddDeviceGroup(string orgID, DeviceGroup deviceGroup, out string dev return true; } + public async Task AddDeviceToGroup(string deviceId, string groupId) + { + using var context = _appDbFactory.GetContext(); + var device = await context.Devices.FirstOrDefaultAsync(x => x.ID == deviceId); + + if (device is null) + { + return Result.Fail("Device not found."); + } + + var group = await context.DeviceGroups.FirstOrDefaultAsync(x => + x.OrganizationID == device.OrganizationID && + x.ID == groupId); + + if (group is null) + { + return Result.Fail("Group not found."); + } + + group.Devices ??= new(); + group.Devices.Add(device); + device.DeviceGroup = group; + device.DeviceGroupID = group.ID; + await context.SaveChangesAsync(); + + return Result.Ok(); + } + public InviteLink AddInvite(string orgID, InviteViewModel invite) { using var dbContext = _appDbFactory.GetContext(); @@ -323,58 +356,62 @@ public InviteLink AddInvite(string orgID, InviteViewModel invite) return inviteLink; } - public async Task> AddOrUpdateDevice(Device device) + public async Task> AddOrUpdateDevice(DeviceClientDto deviceDto) { using var dbContext = _appDbFactory.GetContext(); - var resultDevice = await dbContext.Devices.FindAsync(device.ID); - if (resultDevice != null) + var device = await dbContext.Devices.FindAsync(deviceDto.Id); + + if (device is null) { - resultDevice.CurrentUser = device.CurrentUser; - resultDevice.DeviceName = device.DeviceName; - resultDevice.Drives = device.Drives; - resultDevice.CpuUtilization = device.CpuUtilization; - resultDevice.UsedMemory = device.UsedMemory; - resultDevice.UsedStorage = device.UsedStorage; - resultDevice.Is64Bit = device.Is64Bit; - resultDevice.IsOnline = true; - resultDevice.OSArchitecture = device.OSArchitecture; - resultDevice.OSDescription = device.OSDescription; - resultDevice.Platform = device.Platform; - resultDevice.ProcessorCount = device.ProcessorCount; - resultDevice.PublicIP = device.PublicIP; - resultDevice.TotalMemory = device.TotalMemory; - resultDevice.TotalStorage = device.TotalStorage; - resultDevice.AgentVersion = device.AgentVersion; - resultDevice.LastOnline = DateTimeOffset.Now; + device = new Device + { + OrganizationID = deviceDto.OrganizationId, + ID = deviceDto.Id, + }; + await dbContext.Devices.AddAsync(device); } - else + + device.CurrentUser = deviceDto.CurrentUser; + device.DeviceName = deviceDto.DeviceName; + device.Drives = deviceDto.Drives; + device.CpuUtilization = deviceDto.CpuUtilization; + device.UsedMemory = deviceDto.UsedMemory; + device.UsedStorage = deviceDto.UsedStorage; + device.Is64Bit = deviceDto.Is64Bit; + device.IsOnline = true; + device.OSArchitecture = deviceDto.OsArchitecture; + device.OSDescription = deviceDto.OsDescription; + device.Platform = deviceDto.Platform; + device.ProcessorCount = deviceDto.ProcessorCount; + device.PublicIP = deviceDto.PublicIP; + device.TotalMemory = deviceDto.TotalMemory; + device.TotalStorage = deviceDto.TotalStorage; + device.AgentVersion = deviceDto.AgentVersion; + device.MacAddresses = deviceDto.MacAddresses ?? Array.Empty(); + device.LastOnline = DateTimeOffset.Now; + + if (_hostEnvironment.IsDevelopment() && dbContext.Organizations.Any()) { - device.LastOnline = DateTimeOffset.Now; - if (_hostEnvironment.IsDevelopment() && dbContext.Organizations.Any()) - { - var org = await dbContext.Organizations.FirstOrDefaultAsync(); - device.Organization = org; - device.OrganizationID = org?.ID; - } + var org = await dbContext.Organizations.FirstOrDefaultAsync(); + device.Organization = org; + device.OrganizationID = org?.ID; + } - resultDevice = device; + if (!await dbContext.Organizations.AnyAsync(x => x.ID == device.OrganizationID)) + { + _logger.LogInformation( + "Unable to add device {deviceName} because organization {organizationID}" + + "does not exist. Device ID: {ID}.", + device.DeviceName, + device.OrganizationID, + device.ID); - if (!await dbContext.Organizations.AnyAsync(x => x.ID == device.OrganizationID)) - { - _logger.LogInformation( - "Unable to add device {deviceName} because organization {organizationID}" + - "does not exist. Device ID: {ID}.", - device.DeviceName, - device.OrganizationID, - device.ID); - - return Result.Fail("Organization does not exist."); - } - await dbContext.Devices.AddAsync(device); + return Result.Fail("Organization does not exist."); } + await dbContext.SaveChangesAsync(); - return Result.Ok(resultDevice); + return Result.Ok(device); } public async Task AddOrUpdateSavedScript(SavedScript script, string userId) @@ -920,7 +957,6 @@ public bool DoesUserHaveAccessToDevice(string deviceID, RemotelyUser remotelyUse device.ID == deviceID && ( remotelyUser.IsAdministrator || - string.IsNullOrWhiteSpace(device.DeviceGroupID) || device.DeviceGroup.Users.Any(user => user.Id == remotelyUser.Id ))); } @@ -946,7 +982,6 @@ public string[] FilterDeviceIDsByUserPermission(string[] deviceIDs, RemotelyUser deviceIDs.Contains(device.ID) && ( remotelyUser.IsAdministrator || - string.IsNullOrWhiteSpace(device.DeviceGroupID) || device.DeviceGroup.Users.Any(user => user.Id == remotelyUser.Id ))) .Select(x => x.ID) @@ -1154,22 +1189,39 @@ public int GetDeviceCount(RemotelyUser user) { using var dbContext = _appDbFactory.GetContext(); - return dbContext.Devices - .Include(x => x.DeviceGroup) - .ThenInclude(x => x.Users) - .Count(x => - x.OrganizationID == user.OrganizationID && - ( - user.IsAdministrator || - string.IsNullOrWhiteSpace(x.DeviceGroupID) || - x.DeviceGroup.Users.Any(deviceUser => deviceUser.Id == user.Id) - )); + if (user.IsAdministrator) + { + return GetDeviceCount(); + } + + return dbContext.Users + .Include(x => x.DeviceGroups) + .ThenInclude(x => x.Devices) + .Where(x => x.Id == user.Id) + .SelectMany(x => x.DeviceGroups) + .SelectMany(x => x.Devices) + .Count(); } - public async Task GetDeviceGroup(string deviceGroupID) + public async Task GetDeviceGroup( + string deviceGroupID, + bool includeDevices = false, + bool includeUsers = false) { using var dbContext = _appDbFactory.GetContext(); - return await dbContext.DeviceGroups.FindAsync(deviceGroupID); + + var query = dbContext.DeviceGroups.AsQueryable(); + + if (includeDevices) + { + query = query.Include(x => x.Devices); + } + if (includeUsers) + { + query = query.Include(x => x.Users); + } + + return await query.FirstOrDefaultAsync(x => x.ID == deviceGroupID); } public DeviceGroup[] GetDeviceGroups(string username) @@ -1237,27 +1289,30 @@ public Device[] GetDevicesForUser(string userName) return Array.Empty(); } - var user = dbContext.Users.FirstOrDefault(x => x.UserName == userName); + var user = dbContext.Users + .AsNoTracking() + .FirstOrDefault(x => x.UserName == userName); if (user is null) { return Array.Empty(); } - var deviceIds = dbContext.Devices - .Include(x => x.DeviceGroup) - .ThenInclude(x => x.Users) - .Where(x => - x.OrganizationID == user.OrganizationID && - ( - user.IsAdministrator || - string.IsNullOrWhiteSpace(x.DeviceGroupID) || - x.DeviceGroup.Users.Any(deviceUser => deviceUser.Id == user.Id) - )) - .Select(x => x.ID); + if (user.IsAdministrator) + { + return dbContext.Devices + .AsNoTracking() + .Where(x => x.OrganizationID == user.OrganizationID) + .ToArray(); + } - return dbContext.Devices - .Where(x => deviceIds.Contains(x.ID)) + return dbContext.Users + .AsNoTracking() + .Include(x => x.DeviceGroups) + .ThenInclude(x => x.Devices) + .Where(x => x.UserName == userName) + .SelectMany(x => x.DeviceGroups) + .SelectMany(x => x.Devices) .ToArray(); } diff --git a/Server/Services/ScriptScheduleDispatcher.cs b/Server/Services/ScriptScheduleDispatcher.cs index 7705820dc..d27730294 100644 --- a/Server/Services/ScriptScheduleDispatcher.cs +++ b/Server/Services/ScriptScheduleDispatcher.cs @@ -19,12 +19,12 @@ public interface IScriptScheduleDispatcher public class ScriptScheduleDispatcher : IScriptScheduleDispatcher { private readonly IDataService _dataService; - private readonly IServiceHubSessionCache _serviceSessionCache; + private readonly IAgentHubSessionCache _serviceSessionCache; private readonly ICircuitConnection _circuitConnection; private readonly ILogger _logger; public ScriptScheduleDispatcher(IDataService dataService, - IServiceHubSessionCache serviceSessionCache, + IAgentHubSessionCache serviceSessionCache, ICircuitConnection circuitConnection, ILogger logger) { diff --git a/Server/Services/ScriptScheduler.cs b/Server/Services/ScriptScheduler.cs index d76cf7a53..48f574ceb 100644 --- a/Server/Services/ScriptScheduler.cs +++ b/Server/Services/ScriptScheduler.cs @@ -61,9 +61,7 @@ public async Task DispatchScriptRuns() { using var scope = _serviceProvider.CreateScope(); var scriptScheduleDispatcher = scope.ServiceProvider.GetRequiredService(); - var dataService = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService>(); - var circuitConnection = scope.ServiceProvider.GetRequiredService(); try { diff --git a/Server/Services/ToastService.cs b/Server/Services/ToastService.cs index 024446520..83db504fc 100644 --- a/Server/Services/ToastService.cs +++ b/Server/Services/ToastService.cs @@ -1,4 +1,5 @@ using Nihs.ConcurrentList; +using Remotely.Server.Enums; using Remotely.Server.Models; using System; using System.Timers; @@ -7,11 +8,20 @@ namespace Remotely.Server.Services { public interface IToastService { - ConcurrentList Toasts { get; } - event EventHandler OnToastsChanged; - void ShowToast(string message, int expirationMillisecond = 3000, string classString = null, string styleOverrides = null); + ConcurrentList Toasts { get; } + void ShowToast( + string message, + int expirationMillisecond = 3000, + string classString = null, + string styleOverrides = null); + + void ShowToast2( + string message, + ToastType toastType = ToastType.Info, + int expirationMillisecond = 3000, + string styleOverrides = null); } public class ToastService : IToastService @@ -52,5 +62,23 @@ public void ShowToast(string message, }; removeToastTimer.Start(); } + + public void ShowToast2( + string message, + ToastType toastType, + int expirationMillisecond = 3000, + string styleOverrides = null) + { + var classString = toastType switch + { + ToastType.Info => "bg-info text-white", + ToastType.Success => "bg-success text-white", + ToastType.Warning => "bg-warning text-white", + ToastType.Error => "bg-danger text-white", + _ => "bg-info text-white" + }; + + ShowToast(message, expirationMillisecond, classString, styleOverrides); + } } } diff --git a/Shared/Dtos/DeviceClientDto.cs b/Shared/Dtos/DeviceClientDto.cs new file mode 100644 index 000000000..cec6402c5 --- /dev/null +++ b/Shared/Dtos/DeviceClientDto.cs @@ -0,0 +1,73 @@ +using Remotely.Shared.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Runtime.Serialization; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Remotely.Shared.Dtos +{ + [DataContract] + public class DeviceClientDto + { + [DataMember] + public string AgentVersion { get; set; } = string.Empty; + + [DataMember] + public double CpuUtilization { get; set; } + + [DataMember] + public string CurrentUser { get; set; } = string.Empty; + + [DataMember] + public string DeviceName { get; set; } = string.Empty; + + [DataMember] + public List Drives { get; set; } = new(); + + [DataMember] + public string Id { get; set; } = string.Empty; + + [DataMember] + public bool Is64Bit { get; set; } + + [DataMember] + public bool IsOnline { get; set; } + + [DataMember] + public string[] MacAddresses { get; set; } = Array.Empty(); + + [DataMember] + public string OrganizationId { get; set; } = string.Empty; + + [DataMember] + public Architecture OsArchitecture { get; set; } + + [DataMember] + public string OsDescription { get; set; } = string.Empty; + + [DataMember] + public string Platform { get; set; } = string.Empty; + + [DataMember] + public int ProcessorCount { get; set; } + + [DataMember] + public string PublicIP { get; set; } = string.Empty; + + [DataMember] + public double TotalMemory { get; set; } + + [DataMember] + public double TotalStorage { get; set; } + + [DataMember] + public double UsedMemory { get; set; } + + [DataMember] + public double UsedStorage { get; set; } + } +} diff --git a/Shared/Extensions/DeviceExtensions.cs b/Shared/Extensions/DeviceExtensions.cs new file mode 100644 index 000000000..a9bcbafac --- /dev/null +++ b/Shared/Extensions/DeviceExtensions.cs @@ -0,0 +1,30 @@ +using Remotely.Shared.Dtos; +using Remotely.Shared.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Remotely.Shared.Extensions +{ + public static class DeviceExtensions + { + private static JsonSerializerOptions _serializerOptions = new() + { + PropertyNameCaseInsensitive = true + }; + + /// + /// A helper method for creating a DeviceClientDto from a Device entity. + /// + /// + /// + public static DeviceClientDto ToDto(this Device device) + { + var json = JsonSerializer.Serialize(device, _serializerOptions); + return JsonSerializer.Deserialize(json, _serializerOptions); + } + } +} diff --git a/Shared/Models/Device.cs b/Shared/Models/Device.cs index 31ad2dd47..ffd2dc7e1 100644 --- a/Shared/Models/Device.cs +++ b/Shared/Models/Device.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; using System.Runtime.InteropServices; using System.Text.Json.Serialization; @@ -49,6 +50,10 @@ public class Device [Display(Name = "Last Online")] public DateTimeOffset LastOnline { get; set; } + [Sortable] + [Display(Name = "MAC Addresses")] + public string[] MacAddresses { get; set; } = Array.Empty(); + [StringLength(5000)] public string Notes { get; set; } @@ -71,19 +76,19 @@ public class Device public int ProcessorCount { get; set; } public string PublicIP { get; set; } - public string ServerVerificationToken { get; set; } - [JsonIgnore] public List ScriptResults { get; set; } [JsonIgnore] public List ScriptRuns { get; set; } + [JsonIgnore] public List ScriptRunsCompleted { get; set; } [JsonIgnore] public List ScriptSchedules { get; set; } + public string ServerVerificationToken { get; set; } [StringLength(200)] [Sortable] [Display(Name = "Tags")] @@ -103,7 +108,17 @@ public class Device [Sortable] [Display(Name = "Memory Used %")] - public double UsedMemoryPercent => UsedMemory / TotalMemory; + public double UsedMemoryPercent + { + get + { + if (TotalMemory == 0) + { + return 0; + } + return UsedMemory / TotalMemory; + } + } [Sortable] [Display(Name = "Storage Used")] @@ -111,6 +126,16 @@ public class Device [Sortable] [Display(Name = "Storage Used %")] - public double UsedStoragePercent => UsedStorage / TotalStorage; + public double UsedStoragePercent + { + get + { + if (TotalStorage == 0) + { + return 0; + } + return UsedStorage / TotalStorage; + } + } } } \ No newline at end of file diff --git a/Tests/LoadTester/Program.cs b/Tests/LoadTester/Program.cs index 3b341c0c0..dc22a1638 100644 --- a/Tests/LoadTester/Program.cs +++ b/Tests/LoadTester/Program.cs @@ -116,7 +116,7 @@ private static async Task StartAgent(int i) { try { - var currentInfo = await _deviceInfo.CreateDevice(device.ID, _organizationId); + var currentInfo = await _deviceInfo.CreateDevice(device.Id, _organizationId); currentInfo.DeviceName = device.DeviceName; await hubConnection.SendAsync("DeviceHeartbeat", currentInfo); } diff --git a/Tests/Server.Tests/AgentHubTests.cs b/Tests/Server.Tests/AgentHubTests.cs index d096fbb40..0b7fa32d6 100644 --- a/Tests/Server.Tests/AgentHubTests.cs +++ b/Tests/Server.Tests/AgentHubTests.cs @@ -1,4 +1,4 @@ -using Immense.RemoteControl.Server.Abstractions; +using Remotely.Shared.Extensions; using Immense.RemoteControl.Server.Hubs; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.SignalR; @@ -9,6 +9,7 @@ using Remotely.Server.Hubs; using Remotely.Server.Models; using Remotely.Server.Services; +using Remotely.Shared.Dtos; using Remotely.Shared.Models; using System; using System.Collections.Generic; @@ -34,14 +35,14 @@ public async Task DeviceCameOnline_BannedByName() var circuitManager = new Mock(); var circuitConnection = new Mock(); circuitManager.Setup(x => x.Connections).Returns(new[] { circuitConnection.Object }); - circuitConnection.Setup(x => x.User).Returns(_testData.Admin1); + circuitConnection.Setup(x => x.User).Returns(_testData.Org1Admin1); var appConfig = new Mock(); var viewerHub = new Mock>(); var expiringTokenService = new Mock(); - var serviceSessionCache = new Mock(); + var serviceSessionCache = new Mock(); var logger = new Mock>(); - appConfig.Setup(x => x.BannedDevices).Returns(new string[] { _testData.Device1.DeviceName }); + appConfig.Setup(x => x.BannedDevices).Returns(new string[] { _testData.Org1Device1.DeviceName }); var hub = new AgentHub( DataService, @@ -57,7 +58,7 @@ public async Task DeviceCameOnline_BannedByName() hubClients.Setup(x => x.Caller).Returns(caller.Object); hub.Clients = hubClients.Object; - Assert.IsFalse(await hub.DeviceCameOnline(_testData.Device1)); + Assert.IsFalse(await hub.DeviceCameOnline(_testData.Org1Device1.ToDto())); hubClients.Verify(x => x.Caller, Times.Once); caller.Verify(x => x.SendCoreAsync("UninstallAgent", It.IsAny(), It.IsAny()), Times.Once); } @@ -71,14 +72,14 @@ public async Task DeviceCameOnline_BannedById() var circuitManager = new Mock(); var circuitConnection = new Mock(); circuitManager.Setup(x => x.Connections).Returns(new[] { circuitConnection.Object }); - circuitConnection.Setup(x => x.User).Returns(_testData.Admin1); + circuitConnection.Setup(x => x.User).Returns(_testData.Org1Admin1); var appConfig = new Mock(); var viewerHub = new Mock>(); var expiringTokenService = new Mock(); - var serviceSessionCache = new Mock(); + var serviceSessionCache = new Mock(); var logger = new Mock>(); - appConfig.Setup(x => x.BannedDevices).Returns(new string[] { _testData.Device1.ID }); + appConfig.Setup(x => x.BannedDevices).Returns(new string[] { _testData.Org1Device1.ID }); var hub = new AgentHub( DataService, @@ -94,7 +95,7 @@ public async Task DeviceCameOnline_BannedById() hubClients.Setup(x => x.Caller).Returns(caller.Object); hub.Clients = hubClients.Object; - Assert.IsFalse(await hub.DeviceCameOnline(_testData.Device1)); + Assert.IsFalse(await hub.DeviceCameOnline(_testData.Org1Device1.ToDto())); hubClients.Verify(x => x.Caller, Times.Once); caller.Verify(x => x.SendCoreAsync("UninstallAgent", It.IsAny(), It.IsAny()), Times.Once); } @@ -106,9 +107,10 @@ public void TestCleanup() } [TestInitialize] - public void TestInit() + public async Task TestInit() { _testData = new TestData(); + await _testData.Init(); DataService = IoCActivator.ServiceProvider.GetRequiredService(); } diff --git a/Tests/Server.Tests/CircuitConnectionTests.cs b/Tests/Server.Tests/CircuitConnectionTests.cs new file mode 100644 index 000000000..0b64025b5 --- /dev/null +++ b/Tests/Server.Tests/CircuitConnectionTests.cs @@ -0,0 +1,482 @@ +#nullable enable +using Castle.Core.Logging; +using Immense.RemoteControl.Server.Services; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Remotely.Server.Hubs; +using Remotely.Server.Services; +using Remotely.Server.Tests.Mocks; +using Remotely.Shared.Dtos; +using Remotely.Shared.Extensions; +using Remotely.Shared.Models; +using Remotely.Tests; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Remotely.Server.Tests +{ + [TestClass] + public class CircuitConnectionTests + { +#nullable disable + private TestData _testData; + private IDataService _dataService; + private Mock _authService; + private Mock _clientAppState; + private HubContextFixture _agentHubContextFixture; + private Mock _appConfig; + private Mock _circuitManager; + private Mock _toastService; + private Mock _expiringTokenService; + private Mock _desktopSessionCache; + private Mock _agentSessionCache; + private Mock> _logger; + private CircuitConnection _circuitConnection; +#nullable enable + + [TestInitialize] + public async Task Init() + { + _testData = new TestData(); + await _testData.Init(); + + _dataService = IoCActivator.ServiceProvider.GetRequiredService(); + _authService = new Mock(); + _clientAppState = new Mock(); + _agentHubContextFixture = new HubContextFixture(); + _appConfig = new Mock(); + _circuitManager = new Mock(); + _toastService = new Mock(); + _expiringTokenService = new Mock(); + _desktopSessionCache = new Mock(); + _agentSessionCache = new Mock(); + _logger = new Mock>(); + + _circuitConnection = new CircuitConnection( + _authService.Object, + _dataService, + _clientAppState.Object, + _agentHubContextFixture.HubContextMock.Object, + _appConfig.Object, + _circuitManager.Object, + _toastService.Object, + _expiringTokenService.Object, + _desktopSessionCache.Object, + _agentSessionCache.Object, + _logger.Object); + } + + [TestMethod] + public async Task WakeDevice_GivenUserIsUnauthorized_Fails() + { + // A standard user won't have access if they aren't in the same + // group as the device. + _circuitConnection.User = _testData.Org1User1; + + // Offline device. + _testData.Org1Device1.PublicIP = "142.251.33.110"; + _testData.Org1Device1.MacAddresses = new[] { "78E3B5A1E45B" }; + _testData.Org1Device1.DeviceGroupID = _testData.Org1Group1.ID; + // Online device. + _testData.Org1Device2.PublicIP = "142.251.33.110"; + // Device in another org that shouldn't receive the command. + _testData.Org2Device1.PublicIP = "142.251.33.110"; + + + var updateResult = await _dataService.AddOrUpdateDevice(_testData.Org1Device1.ToDto()); + Assert.IsTrue(updateResult.IsSuccess); + updateResult = await _dataService.AddOrUpdateDevice(_testData.Org1Device2.ToDto()); + Assert.IsTrue(updateResult.IsSuccess); + updateResult = await _dataService.AddOrUpdateDevice(_testData.Org2Device1.ToDto()); + Assert.IsTrue(updateResult.IsSuccess); + + var addGroupResult = await _dataService.AddDeviceToGroup(_testData.Org1Device1.ID, _testData.Org1Group1.ID); + Assert.IsTrue(addGroupResult.IsSuccess); + addGroupResult = await _dataService.AddDeviceToGroup(_testData.Org1Device2.ID, _testData.Org1Group1.ID); + Assert.IsTrue(addGroupResult.IsSuccess); + addGroupResult = await _dataService.AddDeviceToGroup(_testData.Org2Device1.ID, _testData.Org2Group1.ID); + Assert.IsTrue(addGroupResult.IsSuccess); + + var wakeResult = await _circuitConnection.WakeDevice(_testData.Org1Device1); + Assert.IsFalse(wakeResult.IsSuccess); + + _agentSessionCache.VerifyNoOtherCalls(); + _agentHubContextFixture.HubContextMock.VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task WakeDevice_GivenMatchingPeerByIp_UsesCorrectPeer() + { + _circuitConnection.User = _testData.Org1User1; + + var macAddress = "78E3B5A1E45B"; + + // Offline device. + _testData.Org1Device1.PublicIP = "142.251.33.110"; + _testData.Org1Device1.MacAddresses = new[] { macAddress }; + // Online device. + _testData.Org1Device2.PublicIP = "142.251.33.110"; + // Device in another org that shouldn't receive the command. + _testData.Org2Device1.PublicIP = "142.251.33.110"; + + // Offline device in the same group as user. + var addGroupResult = await _dataService.AddDeviceToGroup(_testData.Org1Device1.ID, _testData.Org1Group1.ID); + Assert.IsTrue(addGroupResult.IsSuccess); + + var addToGroupResult = _dataService.AddUserToDeviceGroup( + _testData.Org1Id, + _testData.Org1Group1.ID, + _testData.Org1User1.UserName, + out _); + + var updateResult = await _dataService.AddOrUpdateDevice(_testData.Org1Device1.ToDto()); + Assert.IsTrue(updateResult.IsSuccess); + updateResult = await _dataService.AddOrUpdateDevice(_testData.Org1Device2.ToDto()); + Assert.IsTrue(updateResult.IsSuccess); + updateResult = await _dataService.AddOrUpdateDevice(_testData.Org2Device1.ToDto()); + Assert.IsTrue(updateResult.IsSuccess); + + _agentSessionCache + .Setup(x => x.GetAllDevices()) + .Returns(new[] + { + _testData.Org1Device2, + _testData.Org2Device1 + }); + + var connectionId = "HQUSIBxiOwNokVH_mYgGyg"; + + _agentSessionCache + .Setup(x => x.TryGetConnectionId(_testData.Org1Device2.ID, out connectionId)) + .Returns(true); + + var wakeResult = await _circuitConnection.WakeDevice(_testData.Org1Device1); + + Assert.IsTrue(addToGroupResult); + Assert.IsTrue(wakeResult.IsSuccess); + + + _agentSessionCache + .Verify(x => x.GetAllDevices(), Times.Once); + + _agentSessionCache + .Verify(x => x.TryGetConnectionId(_testData.Org1Device2.ID, out connectionId), Times.Once); + + _agentHubContextFixture.HubClientsMock + .Verify(x => x.Client(connectionId), Times.Once); + + _agentHubContextFixture.SingleClientProxyMock + .Verify(x => + x.SendCoreAsync( + "WakeDevice", + new object[] { macAddress }, + default), + Times.Once); + + _agentHubContextFixture.SingleClientProxyMock.VerifyNoOtherCalls(); + _agentHubContextFixture.HubContextMock.VerifyNoOtherCalls(); + _agentSessionCache.VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task WakeDevice_GivenMatchingPeerByGroupId_UsesCorrectPeer() + { + _circuitConnection.User = _testData.Org1User1; + + var macAddress = "78E3B5A1E45B"; + + // Offline device. + _testData.Org1Device1.PublicIP = "142.251.33.110"; + _testData.Org1Device1.MacAddresses = new[] { macAddress }; + + var addToGroupResult = _dataService.AddUserToDeviceGroup( + _testData.Org1Id, + _testData.Org1Group1.ID, + _testData.Org1User1.UserName, + out _); + + var updateResult = await _dataService.AddOrUpdateDevice(_testData.Org1Device1.ToDto()); + Assert.IsTrue(updateResult.IsSuccess); + + // Offline device. + var addGroupResult = await _dataService.AddDeviceToGroup(_testData.Org1Device1.ID, _testData.Org1Group1.ID); + Assert.IsTrue(addGroupResult.IsSuccess); + // Online device in the same group and org. Should relay wake command. + addGroupResult = await _dataService.AddDeviceToGroup(_testData.Org1Device2.ID, _testData.Org1Group1.ID); + Assert.IsTrue(addGroupResult.IsSuccess); + // Online device in a different org. Should not receive wake command. + addGroupResult = await _dataService.AddDeviceToGroup(_testData.Org2Device1.ID, _testData.Org2Group1.ID); + Assert.IsTrue(addGroupResult.IsSuccess); + + + _agentSessionCache + .Setup(x => x.GetAllDevices()) + .Returns(new[] + { + _testData.Org1Device2, + _testData.Org2Device1 + }); + + var connectionId = "HQUSIBxiOwNokVH_mYgGyg"; + + _agentSessionCache + .Setup(x => x.TryGetConnectionId(_testData.Org1Device2.ID, out connectionId)) + .Returns(true); + + var wakeResult = await _circuitConnection.WakeDevice(_testData.Org1Device1); + + Assert.IsTrue(addToGroupResult); + Assert.IsTrue(wakeResult.IsSuccess); + + + _agentSessionCache + .Verify(x => x.GetAllDevices(), Times.Once); + + _agentSessionCache + .Verify(x => x.TryGetConnectionId(_testData.Org1Device2.ID, out connectionId), Times.Once); + + _agentHubContextFixture.HubClientsMock + .Verify(x => x.Client(connectionId), Times.Once); + + _agentHubContextFixture.SingleClientProxyMock + .Verify(x => + x.SendCoreAsync( + "WakeDevice", + new object[] { macAddress }, + default), + Times.Once); + + _agentHubContextFixture.SingleClientProxyMock.VerifyNoOtherCalls(); + _agentHubContextFixture.HubContextMock.VerifyNoOtherCalls(); + _agentSessionCache.VerifyNoOtherCalls(); + } + + + [TestMethod] + public async Task WakeDevice_GivenNoMatchingGroupOrIp_DoesNotSend() + { + _circuitConnection.User = _testData.Org1User1; + + var macAddress = "78E3B5A1E45B"; + + // Offline device. + _testData.Org1Device1.PublicIP = "142.251.33.110"; + _testData.Org1Device1.MacAddresses = new[] { macAddress }; + _testData.Org1Device1.DeviceGroupID = _testData.Org1Group1.ID; + // Online device, but in a different group. + _testData.Org1Device2.DeviceGroupID = _testData.Org1Group2.ID; + // Device in another org that shouldn't receive the command. + _testData.Org2Device1.DeviceGroupID = _testData.Org2Group1.ID; + + var addToGroupResult = _dataService.AddUserToDeviceGroup( + _testData.Org1Id, + _testData.Org1Group1.ID, + _testData.Org1User1.UserName, + out _); + + var updateResult = await _dataService.AddOrUpdateDevice(_testData.Org1Device1.ToDto()); + Assert.IsTrue(updateResult.IsSuccess); + updateResult = await _dataService.AddOrUpdateDevice(_testData.Org1Device2.ToDto()); + Assert.IsTrue(updateResult.IsSuccess); + updateResult = await _dataService.AddOrUpdateDevice(_testData.Org2Device1.ToDto()); + Assert.IsTrue(updateResult.IsSuccess); + + + // Offline device. + var addGroupResult = await _dataService.AddDeviceToGroup(_testData.Org1Device1.ID, _testData.Org1Group1.ID); + Assert.IsTrue(addGroupResult.IsSuccess); + + // Online device in a different group. Should not recieve wake command. + addGroupResult = await _dataService.AddDeviceToGroup(_testData.Org1Device2.ID, _testData.Org1Group2.ID); + Assert.IsTrue(addGroupResult.IsSuccess); + + // Online device in a different org. Should not recieve wake command. + addGroupResult = await _dataService.AddDeviceToGroup(_testData.Org2Device1.ID, _testData.Org2Group1.ID); + Assert.IsTrue(addGroupResult.IsSuccess); + + _agentSessionCache + .Setup(x => x.GetAllDevices()) + .Returns(new[] + { + _testData.Org1Device2, + _testData.Org2Device1 + }); + + var wakeResult = await _circuitConnection.WakeDevice(_testData.Org1Device1); + + Assert.IsTrue(addToGroupResult); + Assert.IsTrue(wakeResult.IsSuccess); + + + _agentSessionCache + .Verify(x => x.GetAllDevices(), Times.Once); + + _agentHubContextFixture.SingleClientProxyMock.VerifyNoOtherCalls(); + _agentHubContextFixture.HubContextMock.VerifyNoOtherCalls(); + _agentSessionCache.VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task WakeDevices_GivenPeerIpMatches_UsesCorrectPeer() + { + _circuitConnection.User = _testData.Org1User1; + + var macAddress = "78E3B5A1E45B"; + + // Offline device. + _testData.Org1Device1.PublicIP = "142.251.33.110"; + _testData.Org1Device1.MacAddresses = new[] { macAddress }; + _testData.Org1Device1.DeviceGroupID = _testData.Org1Group1.ID; + // Online device. + _testData.Org1Device2.PublicIP = "142.251.33.110"; + // Device in another org that shouldn't receive the command. + _testData.Org2Device1.PublicIP = "142.251.33.110"; + + // Offline device in the same group as user. + var addGroupResult = await _dataService.AddDeviceToGroup(_testData.Org1Device1.ID, _testData.Org1Group1.ID); + Assert.IsTrue(addGroupResult.IsSuccess); + + var addToGroupResult = _dataService.AddUserToDeviceGroup( + _testData.Org1Id, + _testData.Org1Group1.ID, + _testData.Org1User1.UserName, + out _); + + var updateResult = await _dataService.AddOrUpdateDevice(_testData.Org1Device1.ToDto()); + Assert.IsTrue(updateResult.IsSuccess); + updateResult = await _dataService.AddOrUpdateDevice(_testData.Org1Device2.ToDto()); + Assert.IsTrue(updateResult.IsSuccess); + updateResult = await _dataService.AddOrUpdateDevice(_testData.Org2Device1.ToDto()); + Assert.IsTrue(updateResult.IsSuccess); + + _agentSessionCache + .Setup(x => x.GetAllDevices()) + .Returns(new[] + { + _testData.Org1Device2, + _testData.Org2Device1 + }); + + var connectionId = "HQUSIBxiOwNokVH_mYgGyg"; + + _agentSessionCache + .Setup(x => x.TryGetConnectionId(_testData.Org1Device2.ID, out connectionId)) + .Returns(true); + + var wakeResult = await _circuitConnection.WakeDevices(new[] { _testData.Org1Device1 }); + + Assert.IsTrue(addToGroupResult); + Assert.IsTrue(wakeResult.IsSuccess); + + + _agentSessionCache + .Verify(x => x.GetAllDevices(), Times.Once); + + _agentSessionCache + .Verify(x => x.TryGetConnectionId(_testData.Org1Device2.ID, out connectionId), Times.Once); + + _agentHubContextFixture.HubClientsMock + .Verify(x => x.Client(connectionId), Times.Once); + + _agentHubContextFixture.SingleClientProxyMock + .Verify(x => + x.SendCoreAsync( + "WakeDevice", + new object[] { macAddress }, + default), + Times.Once); + + _agentHubContextFixture.SingleClientProxyMock.VerifyNoOtherCalls(); + _agentHubContextFixture.HubContextMock.VerifyNoOtherCalls(); + _agentSessionCache.VerifyNoOtherCalls(); + } + + [TestMethod] + public async Task WakeDevices_GivenMatchingPeerByGroupId_UsesCorrectPeer() + { + _circuitConnection.User = _testData.Org1User1; + + var macAddress = "78E3B5A1E45B"; + + // Offline device. + _testData.Org1Device1.PublicIP = "142.251.33.110"; + _testData.Org1Device1.MacAddresses = new[] { macAddress }; + _testData.Org1Device1.DeviceGroupID = _testData.Org1Group1.ID; + // Online device. + _testData.Org1Device2.DeviceGroupID = _testData.Org1Group1.ID; + // Device in another org that shouldn't receive the command. + _testData.Org2Device1.DeviceGroupID = _testData.Org2Group1.ID; + + var addToGroupResult = _dataService.AddUserToDeviceGroup( + _testData.Org1Id, + _testData.Org1Group1.ID, + _testData.Org1User1.UserName, + out _); + + var updateResult = await _dataService.AddOrUpdateDevice(_testData.Org1Device1.ToDto()); + Assert.IsTrue(updateResult.IsSuccess); + updateResult = await _dataService.AddOrUpdateDevice(_testData.Org1Device2.ToDto()); + Assert.IsTrue(updateResult.IsSuccess); + updateResult = await _dataService.AddOrUpdateDevice(_testData.Org2Device1.ToDto()); + Assert.IsTrue(updateResult.IsSuccess); + + // Offline device. + var addGroupResult = await _dataService.AddDeviceToGroup(_testData.Org1Device1.ID, _testData.Org1Group1.ID); + Assert.IsTrue(addGroupResult.IsSuccess); + // Online device in the same group and org. Should relay wake command. + addGroupResult = await _dataService.AddDeviceToGroup(_testData.Org1Device2.ID, _testData.Org1Group1.ID); + Assert.IsTrue(addGroupResult.IsSuccess); + // Online device in a different org. Should not receive wake command. + addGroupResult = await _dataService.AddDeviceToGroup(_testData.Org2Device1.ID, _testData.Org2Group1.ID); + Assert.IsTrue(addGroupResult.IsSuccess); + + _agentSessionCache + .Setup(x => x.GetAllDevices()) + .Returns(new[] + { + _testData.Org1Device2, + _testData.Org2Device1 + }); + + var connectionId = "HQUSIBxiOwNokVH_mYgGyg"; + + _agentSessionCache + .Setup(x => x.TryGetConnectionId(_testData.Org1Device2.ID, out connectionId)) + .Returns(true); + + var wakeResult = await _circuitConnection.WakeDevices(new[] { _testData.Org1Device1 }); + + Assert.IsTrue(addToGroupResult); + Assert.IsTrue(wakeResult.IsSuccess); + + + _agentSessionCache + .Verify(x => x.GetAllDevices(), Times.Once); + + _agentSessionCache + .Verify(x => x.TryGetConnectionId(_testData.Org1Device2.ID, out connectionId), Times.Once); + + _agentHubContextFixture.HubClientsMock + .Verify(x => x.Client(connectionId), Times.Once); + + _agentHubContextFixture.SingleClientProxyMock + .Verify(x => + x.SendCoreAsync( + "WakeDevice", + new object[] { macAddress }, + default), + Times.Once); + + _agentHubContextFixture.SingleClientProxyMock.VerifyNoOtherCalls(); + _agentHubContextFixture.HubContextMock.VerifyNoOtherCalls(); + _agentSessionCache.VerifyNoOtherCalls(); + } + } +} diff --git a/Tests/Server.Tests/DataServiceTests.cs b/Tests/Server.Tests/DataServiceTests.cs index dc944d94d..215d027b2 100644 --- a/Tests/Server.Tests/DataServiceTests.cs +++ b/Tests/Server.Tests/DataServiceTests.cs @@ -2,6 +2,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Remotely.Server.Services; +using Remotely.Shared.Dtos; using Remotely.Shared.Models; using Remotely.Shared.Utilities; using System; @@ -14,16 +15,16 @@ namespace Remotely.Tests [TestClass] public class DataServiceTests { + private readonly string _newDeviceID = "NewDeviceName"; private IDataService _dataService; private TestData _testData; - private string _newDeviceID = "NewDeviceName"; [TestMethod] public async Task AddAlert() { - await _dataService.AddAlert(_testData.Device1.ID, _testData.OrganizationID, "Test Message"); + await _dataService.AddAlert(_testData.Org1Device1.ID, _testData.Org1Id, "Test Message"); - var alerts = _dataService.GetAlerts(_testData.Admin1.Id); + var alerts = _dataService.GetAlerts(_testData.Org1Admin1.Id); Assert.AreEqual("Test Message", alerts.First().Message); } @@ -35,10 +36,10 @@ public async Task AddOrUpdateDevice() Assert.IsNull(storedDevice); - var newDevice = new Device() + var newDevice = new DeviceClientDto() { - ID = _newDeviceID, - OrganizationID = _testData.OrganizationID, + Id = _newDeviceID, + OrganizationId = _testData.Org1Id, DeviceName = Environment.MachineName, Is64Bit = Environment.Is64BitOperatingSystem }; @@ -60,7 +61,7 @@ public async Task CreateDevice() { DeviceID = Guid.NewGuid().ToString(), DeviceAlias = "Spare Laptop", - OrganizationID = _testData.OrganizationID + OrganizationID = _testData.Org1Id }; // First call should create and return device. @@ -75,32 +76,46 @@ public async Task CreateDevice() [TestMethod] public void DeviceGroupPermissions() { - Assert.IsTrue(_dataService.GetDevicesForUser(_testData.Admin1.UserName).Count() == 2); - Assert.IsTrue(_dataService.GetDevicesForUser(_testData.Admin2.UserName).Count() == 2); - Assert.IsTrue(_dataService.GetDevicesForUser(_testData.User1.UserName).Count() == 2); - Assert.IsTrue(_dataService.GetDevicesForUser(_testData.User2.UserName).Count() == 2); - - var groupID = _dataService.GetDeviceGroups(_testData.Admin1.UserName).First().ID; - - _dataService.UpdateDevice(_testData.Device1.ID, "", "", groupID, ""); - _dataService.AddUserToDeviceGroup(_testData.OrganizationID, groupID, _testData.User1.UserName, out _); - - Assert.IsTrue(_dataService.GetDevicesForUser(_testData.Admin1.UserName).Count() == 2); - Assert.IsTrue(_dataService.GetDevicesForUser(_testData.Admin2.UserName).Count() == 2); - Assert.IsTrue(_dataService.GetDevicesForUser(_testData.User1.UserName).Count() == 2); - Assert.IsTrue(_dataService.GetDevicesForUser(_testData.User2.UserName).Count() == 1); - - Assert.IsTrue(_dataService.DoesUserHaveAccessToDevice(_testData.Device1.ID, _testData.Admin1)); - Assert.IsTrue(_dataService.DoesUserHaveAccessToDevice(_testData.Device1.ID, _testData.Admin2)); - Assert.IsTrue(_dataService.DoesUserHaveAccessToDevice(_testData.Device1.ID, _testData.User1)); - Assert.IsFalse(_dataService.DoesUserHaveAccessToDevice(_testData.Device1.ID, _testData.User2)); - - var allDevices = _dataService.GetAllDevices(_testData.OrganizationID).Select(x => x.ID).ToArray(); - - Assert.AreEqual(2, _dataService.FilterDeviceIDsByUserPermission(allDevices, _testData.Admin1).Length); - Assert.AreEqual(2, _dataService.FilterDeviceIDsByUserPermission(allDevices, _testData.Admin2).Length); - Assert.AreEqual(2, _dataService.FilterDeviceIDsByUserPermission(allDevices, _testData.User1).Length); - Assert.AreEqual(1, _dataService.FilterDeviceIDsByUserPermission(allDevices, _testData.User2).Length); + Assert.AreEqual(2, _dataService.GetDevicesForUser(_testData.Org1Admin1.UserName).Length); + Assert.AreEqual(2, _dataService.GetDevicesForUser(_testData.Org1Admin2.UserName).Length); + Assert.AreEqual(0, _dataService.GetDevicesForUser(_testData.Org1User1.UserName).Length); + Assert.AreEqual(0, _dataService.GetDevicesForUser(_testData.Org1User2.UserName).Length); + Assert.AreEqual(0, _dataService.GetDevicesForUser(_testData.Org2User1.UserName).Length); + Assert.AreEqual(0, _dataService.GetDevicesForUser(_testData.Org2User2.UserName).Length); + + Assert.IsTrue(_dataService.DoesUserHaveAccessToDevice(_testData.Org1Device1.ID, _testData.Org1Admin1)); + Assert.IsTrue(_dataService.DoesUserHaveAccessToDevice(_testData.Org1Device1.ID, _testData.Org1Admin2)); + Assert.IsFalse(_dataService.DoesUserHaveAccessToDevice(_testData.Org1Device1.ID, _testData.Org1User1)); + Assert.IsFalse(_dataService.DoesUserHaveAccessToDevice(_testData.Org1Device1.ID, _testData.Org1User2)); + Assert.IsFalse(_dataService.DoesUserHaveAccessToDevice(_testData.Org1Device1.ID, _testData.Org2User1)); + Assert.IsFalse(_dataService.DoesUserHaveAccessToDevice(_testData.Org1Device1.ID, _testData.Org2User2)); + + var groupID = _testData.Org1Group1.ID; + _dataService.AddUserToDeviceGroup(_testData.Org1Id, groupID, _testData.Org1User1.UserName, out _); + _testData.Org1Device1.DeviceGroupID = groupID; + _dataService.UpdateDevice(_testData.Org1Device1.ID, "", "", groupID, ""); + + Assert.AreEqual(2, _dataService.GetDevicesForUser(_testData.Org1Admin1.UserName).Length); + Assert.AreEqual(2, _dataService.GetDevicesForUser(_testData.Org1Admin2.UserName).Length); + Assert.AreEqual(1, _dataService.GetDevicesForUser(_testData.Org1User1.UserName).Length); + Assert.AreEqual(0, _dataService.GetDevicesForUser(_testData.Org1User2.UserName).Length); + Assert.AreEqual(0, _dataService.GetDevicesForUser(_testData.Org2User1.UserName).Length); + Assert.AreEqual(0, _dataService.GetDevicesForUser(_testData.Org2User2.UserName).Length); + + Assert.IsTrue(_dataService.DoesUserHaveAccessToDevice(_testData.Org1Device1.ID, _testData.Org1Admin1)); + Assert.IsTrue(_dataService.DoesUserHaveAccessToDevice(_testData.Org1Device1.ID, _testData.Org1Admin2)); + Assert.IsTrue(_dataService.DoesUserHaveAccessToDevice(_testData.Org1Device1.ID, _testData.Org1User1)); + Assert.IsFalse(_dataService.DoesUserHaveAccessToDevice(_testData.Org1Device1.ID, _testData.Org1User2)); + Assert.IsFalse(_dataService.DoesUserHaveAccessToDevice(_testData.Org1Device1.ID, _testData.Org2User1)); + Assert.IsFalse(_dataService.DoesUserHaveAccessToDevice(_testData.Org1Device1.ID, _testData.Org2User2)); + + var allDevices = _dataService.GetAllDevices(_testData.Org1Id).Select(x => x.ID).ToArray(); + Assert.AreEqual(2, _dataService.FilterDeviceIDsByUserPermission(allDevices, _testData.Org1Admin1).Length); + Assert.AreEqual(2, _dataService.FilterDeviceIDsByUserPermission(allDevices, _testData.Org1Admin2).Length); + Assert.AreEqual(1, _dataService.FilterDeviceIDsByUserPermission(allDevices, _testData.Org1User1).Length); + Assert.AreEqual(0, _dataService.FilterDeviceIDsByUserPermission(allDevices, _testData.Org1User2).Length); + Assert.AreEqual(0, _dataService.FilterDeviceIDsByUserPermission(allDevices, _testData.Org2User1).Length); + Assert.AreEqual(0, _dataService.FilterDeviceIDsByUserPermission(allDevices, _testData.Org2User2).Length); } [TestMethod] @@ -111,25 +126,25 @@ public async Task GetPendingScriptRuns_GivenMultipleRunsQueued_ReturnsOnlyLatest var savedScript = new SavedScript() { Content = "Get-ChildItem", - Creator = _testData.Admin1, - CreatorId = _testData.Admin1.Id, + Creator = _testData.Org1Admin1, + CreatorId = _testData.Org1Admin1.Id, Name = "GCI", - Organization = _testData.Admin1.Organization, - OrganizationID = _testData.OrganizationID, + Organization = _testData.Org1Admin1.Organization, + OrganizationID = _testData.Org1Id, Shell = Shared.Enums.ScriptingShell.PSCore }; - await _dataService.AddOrUpdateSavedScript(savedScript, _testData.Admin1.Id); + await _dataService.AddOrUpdateSavedScript(savedScript, _testData.Org1Admin1.Id); var scriptRun = new ScriptRun() { - Devices = new() { _testData.Device1 }, + Devices = new() { _testData.Org1Device1 }, InputType = Shared.Enums.ScriptInputType.ScheduledScript, SavedScriptId = savedScript.Id, - Initiator = _testData.Admin1.UserName, + Initiator = _testData.Org1Admin1.UserName, RunAt = now, - OrganizationID = _testData.OrganizationID, - Organization = _testData.Admin1.Organization, + OrganizationID = _testData.Org1Id, + Organization = _testData.Org1Admin1.Organization, RunOnNextConnect = true }; @@ -142,16 +157,16 @@ public async Task GetPendingScriptRuns_GivenMultipleRunsQueued_ReturnsOnlyLatest Time.Adjust(TimeSpan.FromMinutes(2)); - var pendingRuns = await _dataService.GetPendingScriptRuns(_testData.Device1.ID); + var pendingRuns = await _dataService.GetPendingScriptRuns(_testData.Org1Device1.ID); Assert.AreEqual(1, pendingRuns.Count); Assert.AreEqual(2, pendingRuns[0].Id); var scriptResult = new ScriptResult() { - DeviceID = _testData.Device1.ID, + DeviceID = _testData.Org1Device1.ID, InputType = Shared.Enums.ScriptInputType.ScheduledScript, - OrganizationID = _testData.OrganizationID, + OrganizationID = _testData.Org1Id, SavedScriptId = savedScript.Id, ScriptRunId = scriptRun.Id, Shell = Shared.Enums.ScriptingShell.PSCore @@ -161,7 +176,7 @@ public async Task GetPendingScriptRuns_GivenMultipleRunsQueued_ReturnsOnlyLatest await _dataService.AddScriptResultToScriptRun(scriptResult.ID, scriptRun.Id); - pendingRuns = await _dataService.GetPendingScriptRuns(_testData.Device1.ID); + pendingRuns = await _dataService.GetPendingScriptRuns(_testData.Org1Device1.ID); Assert.AreEqual(0, pendingRuns.Count); } @@ -174,26 +189,19 @@ public void TestCleanup() } [TestInitialize] - public void TestInit() + public async Task TestInit() { _testData = new TestData(); + await _testData.Init(); _dataService = IoCActivator.ServiceProvider.GetRequiredService(); - - var newDevice = new Device() - { - ID = _newDeviceID, - DeviceName = Environment.MachineName, - Is64Bit = Environment.Is64BitOperatingSystem, - OrganizationID = _testData.OrganizationID - }; } [TestMethod] public void UpdateOrganizationName() { - Assert.IsTrue(string.IsNullOrWhiteSpace(_testData.Admin1.Organization.OrganizationName)); - _dataService.UpdateOrganizationName(_testData.OrganizationID, "Test Org"); - var updatedOrg = _dataService.GetOrganizationById(_testData.OrganizationID); + Assert.IsTrue(string.IsNullOrWhiteSpace(_testData.Org1Admin1.Organization.OrganizationName)); + _dataService.UpdateOrganizationName(_testData.Org1Id, "Test Org"); + var updatedOrg = _dataService.GetOrganizationById(_testData.Org1Id); Assert.AreEqual("Test Org", updatedOrg.OrganizationName); } @@ -204,57 +212,75 @@ public async Task UpdateServerAdmins() Assert.AreEqual(1, currentAdmins.Count); - await _dataService.SetIsServerAdmin(_testData.Admin2.Id, true, _testData.Admin1.Id); + await _dataService.SetIsServerAdmin(_testData.Org1Admin2.Id, true, _testData.Org1Admin1.Id); currentAdmins = _dataService.GetServerAdmins(); Assert.AreEqual(2, currentAdmins.Count); - Assert.IsTrue(currentAdmins.Contains(_testData.Admin1.UserName)); - Assert.IsTrue(currentAdmins.Contains(_testData.Admin2.UserName)); + Assert.IsTrue(currentAdmins.Contains(_testData.Org1Admin1.UserName)); + Assert.IsTrue(currentAdmins.Contains(_testData.Org1Admin2.UserName)); // Shouldn't be able to change themselves. - await _dataService.SetIsServerAdmin(_testData.Admin2.Id, false, _testData.Admin2.Id); + await _dataService.SetIsServerAdmin(_testData.Org1Admin2.Id, false, _testData.Org1Admin2.Id); currentAdmins = _dataService.GetServerAdmins(); Assert.AreEqual(2, currentAdmins.Count); // Non-admins shouldn't be able to change admins. - await _dataService.SetIsServerAdmin(_testData.User1.Id, false, _testData.Admin2.Id); + await _dataService.SetIsServerAdmin(_testData.Org1User1.Id, false, _testData.Org1Admin2.Id); currentAdmins = _dataService.GetServerAdmins(); Assert.AreEqual(2, currentAdmins.Count); - await _dataService.SetIsServerAdmin(_testData.Admin2.Id, false, _testData.Admin1.Id); + await _dataService.SetIsServerAdmin(_testData.Org1Admin2.Id, false, _testData.Org1Admin1.Id); currentAdmins = _dataService.GetServerAdmins(); Assert.AreEqual(1, currentAdmins.Count); - Assert.AreEqual(_testData.Admin1.UserName, currentAdmins[0]); + Assert.AreEqual(_testData.Org1Admin1.UserName, currentAdmins[0]); } [TestMethod] public void VerifyInitialData() { - Assert.IsNotNull(_dataService.GetUserByNameWithOrg(_testData.Admin1.UserName)); - Assert.IsNotNull(_dataService.GetUserByNameWithOrg(_testData.Admin2.UserName)); - Assert.IsNotNull(_dataService.GetUserByNameWithOrg(_testData.User1.UserName)); - Assert.IsNotNull(_dataService.GetUserByNameWithOrg(_testData.User2.UserName)); - Assert.AreEqual(1, _dataService.GetOrganizationCount()); - - var devices = _dataService.GetAllDevices(_testData.OrganizationID); - - Assert.AreEqual(2, devices.Count()); - Assert.IsTrue(devices.Any(x => x.ID == "Device1")); - Assert.IsTrue(devices.Any(x => x.ID == "Device2")); - - var orgIDs = new string[] + Assert.IsNotNull(_dataService.GetUserByNameWithOrg(_testData.Org1Admin1.UserName)); + Assert.IsNotNull(_dataService.GetUserByNameWithOrg(_testData.Org1Admin2.UserName)); + Assert.IsNotNull(_dataService.GetUserByNameWithOrg(_testData.Org1User1.UserName)); + Assert.IsNotNull(_dataService.GetUserByNameWithOrg(_testData.Org1User2.UserName)); + Assert.AreEqual(2, _dataService.GetOrganizationCount()); + + var devices1 = _dataService.GetAllDevices(_testData.Org1Id); + Assert.AreEqual(2, devices1.Length); + Assert.IsTrue(devices1.Any(x => x.ID == "Org1Device1")); + Assert.IsTrue(devices1.Any(x => x.ID == "Org1Device2")); + + var devices2 = _dataService.GetAllDevices(_testData.Org2Id); + Assert.AreEqual(2, devices2.Length); + Assert.IsTrue(devices2.Any(x => x.ID == "Org2Device1")); + Assert.IsTrue(devices2.Any(x => x.ID == "Org2Device2")); + + var org1Ids = new string[] { - _testData.Group1.OrganizationID, - _testData.Group2.OrganizationID, - _testData.Admin1.OrganizationID, - _testData.Admin2.OrganizationID, - _testData.User1.OrganizationID, - _testData.User2.OrganizationID, - _testData.Device1.OrganizationID, - _testData.Device2.OrganizationID + _testData.Org1Group1.OrganizationID, + _testData.Org1Group2.OrganizationID, + _testData.Org1Admin1.OrganizationID, + _testData.Org1Admin2.OrganizationID, + _testData.Org1User1.OrganizationID, + _testData.Org1User2.OrganizationID, + _testData.Org1Device1.OrganizationID, + _testData.Org1Device2.OrganizationID }; - Assert.IsTrue(orgIDs.All(x => x == _testData.OrganizationID)); + Assert.IsTrue(org1Ids.All(x => x == _testData.Org1Id)); + + var org2Ids = new string[] +{ + _testData.Org2Group1.OrganizationID, + _testData.Org2Group2.OrganizationID, + _testData.Org2Admin1.OrganizationID, + _testData.Org2Admin2.OrganizationID, + _testData.Org2User1.OrganizationID, + _testData.Org2User2.OrganizationID, + _testData.Org2Device1.OrganizationID, + _testData.Org2Device2.OrganizationID +}; + + Assert.IsTrue(org2Ids.All(x => x == _testData.Org2Id)); } } } diff --git a/Tests/Server.Tests/Mocks/HubContextFixture.cs b/Tests/Server.Tests/Mocks/HubContextFixture.cs new file mode 100644 index 000000000..c5451c5f3 --- /dev/null +++ b/Tests/Server.Tests/Mocks/HubContextFixture.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.SignalR; +using Moq; +using Remotely.Server.Hubs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Remotely.Server.Tests.Mocks +{ + public class HubContextFixture + where T : Hub + { + public HubContextFixture() + { + HubContextMock = new Mock>(); + HubClientsMock = new Mock(); + GroupManagerMock = new Mock(); + SingleClientProxyMock = new Mock(); + ClientProxyMock = new Mock(); + + HubContextMock + .Setup(x => x.Clients) + .Returns(HubClientsMock.Object); + + HubContextMock + .Setup(x => x.Groups) + .Returns(GroupManagerMock.Object); + + HubClientsMock + .Setup(x => x.Client(It.IsAny())) + .Returns(SingleClientProxyMock.Object); + + HubClientsMock + .Setup(x => x.Group(It.IsAny())) + .Returns(ClientProxyMock.Object); + } + + public Mock> HubContextMock { get; } + public Mock HubClientsMock { get; } + public Mock GroupManagerMock { get; } + public Mock SingleClientProxyMock { get; } + public Mock ClientProxyMock { get; } + } +} diff --git a/Tests/Server.Tests/ScriptScheduleDispatcherTests.cs b/Tests/Server.Tests/ScriptScheduleDispatcherTests.cs index 976f1b65c..ca5d64d4d 100644 --- a/Tests/Server.Tests/ScriptScheduleDispatcherTests.cs +++ b/Tests/Server.Tests/ScriptScheduleDispatcherTests.cs @@ -22,16 +22,17 @@ public class ScriptScheduleDispatcherTests private ScriptSchedule _schedule1; private Mock _dataService; private Mock _circuitConnection; - private Mock _serviceSessionCache; + private Mock _serviceSessionCache; private Mock> _logger; private ScriptScheduleDispatcher _dispatcher; private TestData _testData; private SavedScript _savedScript; [TestInitialize] - public void Init() + public async Task Init() { _testData = new TestData(); + await _testData.Init(); _savedScript = new SavedScript() { @@ -41,10 +42,10 @@ public void Init() _schedule1 = new() { CreatedAt = Time.Now, - CreatorId = _testData.User1.Id, + CreatorId = _testData.Org1User1.Id, Devices = new List() { - _testData.Device1 + _testData.Org1Device1 }, DeviceGroups = new List() { @@ -52,7 +53,7 @@ public void Init() { Devices = new List() { - _testData.Device2 + _testData.Org1Device2 } } }, @@ -60,7 +61,7 @@ public void Init() Name = "_scheduleName", Id = 5, NextRun = Time.Now.AddMinutes(1), - OrganizationID = _testData.User1.OrganizationID, + OrganizationID = _testData.Org1User1.OrganizationID, SavedScriptId = _savedScript.Id }; @@ -72,12 +73,12 @@ public void Init() _dataService = new Mock(); _dataService.Setup(x => x.GetScriptSchedulesDue()).Returns(Task.FromResult(scriptSchedules)); _dataService.Setup(x => x.GetDevices(It.Is>(x => - x.Contains(_testData.Device1.ID) && - x.Contains(_testData.Device2.ID) - ))).Returns(new List() { _testData.Device1, _testData.Device2 }); + x.Contains(_testData.Org1Device1.ID) && + x.Contains(_testData.Org1Device2.ID) + ))).Returns(new List() { _testData.Org1Device1, _testData.Org1Device2 }); _circuitConnection = new Mock(); - _serviceSessionCache = new Mock(); + _serviceSessionCache = new Mock(); _logger = new Mock>(); _dispatcher = new ScriptScheduleDispatcher(_dataService.Object, _serviceSessionCache.Object, _circuitConnection.Object, _logger.Object); } @@ -109,8 +110,8 @@ public async Task DispatchPendingScriptRuns_GivenSchedulesDue_CreatesScriptRuns( x.Contains(_schedule1.Devices.First().ID)))); _dataService.Verify(x => x.AddScriptRun(It.Is(x => x.ScheduleId == _schedule1.Id && - x.Devices.Exists(d => d.ID == _testData.Device1.ID) && - x.Devices.Exists(d => d.ID == _testData.Device2.ID)))); + x.Devices.Exists(d => d.ID == _testData.Org1Device1.ID) && + x.Devices.Exists(d => d.ID == _testData.Org1Device2.ID)))); _dataService.VerifyNoOtherCalls(); _circuitConnection.Verify(x => x.RunScript( diff --git a/Tests/Server.Tests/TestData.cs b/Tests/Server.Tests/TestData.cs index 221200460..968e315a0 100644 --- a/Tests/Server.Tests/TestData.cs +++ b/Tests/Server.Tests/TestData.cs @@ -1,9 +1,11 @@ using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Moq; using Remotely.Server.Areas.Identity.Pages.Account.Manage; using Remotely.Server.Data; using Remotely.Server.Services; +using Remotely.Shared.Dtos; using Remotely.Shared.Models; using System; using System.Linq; @@ -14,97 +16,158 @@ namespace Remotely.Tests { public class TestData { - public TestData() - { - Init().Wait(); - } + #region Organization1 + public Organization Org1 => Org1Admin1.Organization; - public RemotelyUser Admin1 { get; } = new RemotelyUser() + public RemotelyUser Org1Admin1 { get; } = new() { - UserName = "admin1@test.com", + UserName = "org1admin1@test.com", IsAdministrator = true, IsServerAdmin = true, Organization = new Organization(), UserOptions = new RemotelyUserOptions() }; - public RemotelyUser Admin2 { get; private set; } + public RemotelyUser Org1Admin2 { get; private set; } + + public Device Org1Device1 { get; private set; } - public Device Device1 { get; private set; } = new Device() + public Device Org1Device2 { get; private set; } + + public DeviceGroup Org1Group1 { get; private set; } = new DeviceGroup() { - ID = "Device1", - DeviceName = "Device1Name" + Name = "Org1Group1" }; - public Device Device2 { get; private set; } = new Device() + public DeviceGroup Org1Group2 { get; private set; } = new DeviceGroup() { - ID = "Device2", - DeviceName = "Device2Name" + Name = "Org1Group2" }; - public DeviceGroup Group1 { get; private set; } = new DeviceGroup() + public string Org1Id => Org1.ID; + public RemotelyUser Org1User1 { get; private set; } + public RemotelyUser Org1User2 { get; private set; } + #endregion + + + + #region Organization2 + public Organization Org2 => Org2Admin1.Organization; + + public RemotelyUser Org2Admin1 { get; } = new() { - Name = "Group1" + UserName = "org2admin1@test.com", + IsAdministrator = true, + IsServerAdmin = false, + Organization = new Organization(), + UserOptions = new RemotelyUserOptions() }; - public DeviceGroup Group2 { get; private set; } = new DeviceGroup() + public RemotelyUser Org2Admin2 { get; private set; } + + public Device Org2Device1 { get; private set; } + + public Device Org2Device2 { get; private set; } + + public DeviceGroup Org2Group1 { get; private set; } = new DeviceGroup() { - Name = "Group2" + Name = "Org2Group1" }; - public string OrganizationID { get; private set; } - - public RemotelyUser User1 { get; private set; } + public DeviceGroup Org2Group2 { get; private set; } = new DeviceGroup() + { + Name = "Org2Group2" + }; - public RemotelyUser User2 { get; private set; } + public string Org2Id => Org2.ID; + public RemotelyUser Org2User1 { get; private set; } + public RemotelyUser Org2User2 { get; private set; } + #endregion public void ClearData() { - var dbContext = IoCActivator.ServiceProvider.GetRequiredService(); - dbContext.Devices.RemoveRange(dbContext.Devices.ToList()); - dbContext.DeviceGroups.RemoveRange(dbContext.DeviceGroups.ToList()); - dbContext.Users.RemoveRange(dbContext.Users.ToList()); - dbContext.Organizations.RemoveRange(dbContext.Organizations.ToList()); - dbContext.Alerts.RemoveRange(dbContext.Alerts.ToList()); - dbContext.ScriptResults.RemoveRange(dbContext.ScriptResults.ToList()); - dbContext.ScriptRuns.RemoveRange(dbContext.ScriptRuns.ToList()); - dbContext.ScriptSchedules.RemoveRange(dbContext.ScriptSchedules.ToList()); - dbContext.SavedScripts.RemoveRange(dbContext.SavedScripts.ToList()); - dbContext.SaveChanges(); + using var scope = IoCActivator.ServiceProvider.CreateScope(); + using var dbContext = scope.ServiceProvider.GetRequiredService(); + dbContext.Database.EnsureDeleted(); + dbContext.Database.EnsureCreated(); } - private async Task Init() + public async Task Init() { ClearData(); + using var scope = IoCActivator.ServiceProvider.CreateScope(); + using var userManager = scope.ServiceProvider.GetRequiredService>(); var dataService = IoCActivator.ServiceProvider.GetRequiredService(); - var userManager = IoCActivator.ServiceProvider.GetRequiredService>(); var emailSender = IoCActivator.ServiceProvider.GetRequiredService(); - await userManager.CreateAsync(Admin1); - - await dataService.CreateUser("admin2@test.com", true, Admin1.OrganizationID); - Admin2 = dataService.GetUserByNameWithOrg("admin2@test.com"); - - await dataService.CreateUser("testuser1@test.com", false, Admin1.OrganizationID); - User1 = dataService.GetUserByNameWithOrg("testuser1@test.com"); - - await dataService.CreateUser("testuser2@test.com", false, Admin1.OrganizationID); - User2 = dataService.GetUserByNameWithOrg("testuser2@test.com"); - - Device1.OrganizationID = Admin1.OrganizationID; - await dataService.AddOrUpdateDevice(Device1); ; - Device2.OrganizationID = Admin1.OrganizationID; - await dataService.AddOrUpdateDevice(Device2); - - dataService.AddDeviceGroup(Admin1.OrganizationID, Group1, out _, out _); - dataService.AddDeviceGroup(Admin1.OrganizationID, Group2, out _, out _); - var deviceGroups = dataService.GetDeviceGroups(Admin1.UserName); - Group1 = deviceGroups.First(x => x.Name == Group1.Name); - Group2 = deviceGroups.First(x => x.Name == Group2.Name); - - OrganizationID = Admin1.OrganizationID; + // Organization 1 + await userManager.CreateAsync(Org1Admin1); + + await dataService.CreateUser("org1admin2@test.com", true, Org1Admin1.OrganizationID); + Org1Admin2 = dataService.GetUserByNameWithOrg("org1admin2@test.com"); + + await dataService.CreateUser("org1testuser1@test.com", false, Org1Admin1.OrganizationID); + Org1User1 = dataService.GetUserByNameWithOrg("org1testuser1@test.com"); + + await dataService.CreateUser("org1testuser2@test.com", false, Org1Admin1.OrganizationID); + Org1User2 = dataService.GetUserByNameWithOrg("org1testuser2@test.com"); + + var device1 = new DeviceClientDto() + { + Id = "Org1Device1", + DeviceName = "Org1Device1Name", + OrganizationId = Org1Id + }; + var device2 = new DeviceClientDto() + { + Id = "Org1Device2", + DeviceName = "Org1Device2Name", + OrganizationId = Org1Id + }; + Org1Device1 = (await dataService.AddOrUpdateDevice(device1)).Value; + Org1Device2 = (await dataService.AddOrUpdateDevice(device2)).Value; + + dataService.AddDeviceGroup(Org1Admin1.OrganizationID, Org1Group1, out _, out _); + dataService.AddDeviceGroup(Org1Admin1.OrganizationID, Org1Group2, out _, out _); + var deviceGroups1 = dataService.GetDeviceGroups(Org1Admin1.UserName); + Org1Group1 = deviceGroups1.First(x => x.Name == Org1Group1.Name); + Org1Group2 = deviceGroups1.First(x => x.Name == Org1Group2.Name); + + + // Organization 2 + await userManager.CreateAsync(Org2Admin1); + + await dataService.CreateUser("org2admin2@test.com", true, Org2Admin1.OrganizationID); + Org2Admin2 = dataService.GetUserByNameWithOrg("org2admin2@test.com"); + + await dataService.CreateUser("org2testuser1@test.com", false, Org2Admin1.OrganizationID); + Org2User1 = dataService.GetUserByNameWithOrg("org2testuser1@test.com"); + + await dataService.CreateUser("org2testuser2@test.com", false, Org2Admin1.OrganizationID); + Org2User2 = dataService.GetUserByNameWithOrg("org2testuser2@test.com"); + + var device3 = new DeviceClientDto() + { + Id = "Org2Device1", + DeviceName = "Org2Device1Name", + OrganizationId = Org2Id + }; + var device4 = new DeviceClientDto() + { + Id = "Org2Device2", + DeviceName = "Org2Device2Name", + OrganizationId = Org2Id + }; + Org2Device1 = (await dataService.AddOrUpdateDevice(device3)).Value; + Org2Device2 = (await dataService.AddOrUpdateDevice(device4)).Value; + + dataService.AddDeviceGroup(Org2Admin1.OrganizationID, Org2Group1, out _, out _); + dataService.AddDeviceGroup(Org2Admin1.OrganizationID, Org2Group2, out _, out _); + var deviceGroups2 = dataService.GetDeviceGroups(Org2Admin1.UserName); + Org2Group1 = deviceGroups2.First(x => x.Name == Org2Group1.Name); + Org2Group2 = deviceGroups2.First(x => x.Name == Org2Group2.Name); } } } diff --git a/submodules/Immense.RemoteControl b/submodules/Immense.RemoteControl index 2e6914d88..2c722cecd 160000 --- a/submodules/Immense.RemoteControl +++ b/submodules/Immense.RemoteControl @@ -1 +1 @@ -Subproject commit 2e6914d88aa2d46aec41f1baff10ab7d15d62c5a +Subproject commit 2c722cecdd6502b2524526e5e15564a8db9f4bad