From e5470f9d5f920c36b8857f15690dd21e1836650b Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Mon, 24 Jul 2023 09:16:22 -0700 Subject: [PATCH 01/29] First pass of nullable references. --- .../Agent.Installer.Win.csproj | 2 + Agent.Installer.Win/App.xaml.cs | 13 +- Agent.Installer.Win/MainWindow.xaml.cs | 108 +-- Agent.Installer.Win/Models/BrandingInfo.cs | 35 +- .../Models/EmbeddedServerData.cs | 42 +- .../Services/EmbeddedServerDataReader.cs | 93 ++- .../Services/InstallerService.cs | 643 ++++++++------- Agent.Installer.Win/Services/RelayCommand.cs | 53 +- .../Utilities/CommandLineParser.cs | 98 +-- Agent.Installer.Win/Utilities/Logger.cs | 97 ++- Agent.Installer.Win/Utilities/MessageBoxEx.cs | 15 +- Agent.Installer.Win/Utilities/ProcessEx.cs | 23 +- .../ViewModels/MainWindowViewModel.cs | 766 +++++++++--------- .../ViewModels/ViewModelBase.cs | 19 +- Agent/Agent.csproj | 1 + Agent/Program.cs | 4 +- Desktop.Linux/Desktop.Linux.csproj | 1 + Desktop.Win/Desktop.Win.csproj | 1 + Server/API/AlertsController.cs | 17 +- .../API/OrganizationManagementController.cs | 13 +- .../Identity/Pages/Account/Register.cshtml.cs | 2 +- .../Components/Scripts/SavedScripts.razor.cs | 22 +- .../Scripts/ScriptSchedules.razor.cs | 5 +- Server/Pages/ApiKeys.razor | 27 +- Server/Pages/ManageOrganization.razor.cs | 18 +- Server/Server.csproj | 1 + Server/Services/DataService.cs | 44 +- Shared/AppConstants.cs | 22 +- Shared/Extensions/DeviceExtensions.cs | 4 +- Shared/Models/Alert.cs | 20 +- Shared/Models/AlertOptions.cs | 18 +- Shared/Models/ApiToken.cs | 10 +- Shared/Models/ConnectionInfo.cs | 35 +- Shared/Models/Device.cs | 43 +- Shared/Models/DeviceGroup.cs | 14 +- Shared/Models/DeviceSetupOptions.cs | 16 +- Shared/Models/Drive.cs | 8 +- Shared/Models/InviteLink.cs | 10 +- Shared/Models/Organization.cs | 28 +- Shared/Models/RemotelyUser.cs | 16 +- Shared/Models/SavedScript.cs | 16 +- Shared/Models/ScriptResult.cs | 22 +- Shared/Shared.csproj | 1 + Shared/ViewModels/ViewModelBase.cs | 15 +- Tests/Server.Tests/DataServiceTests.cs | 5 +- .../ScriptScheduleDispatcherTests.cs | 4 +- Tests/Server.Tests/TestData.cs | 12 +- 47 files changed, 1267 insertions(+), 1215 deletions(-) diff --git a/Agent.Installer.Win/Agent.Installer.Win.csproj b/Agent.Installer.Win/Agent.Installer.Win.csproj index de9ce28ba..cc7634142 100644 --- a/Agent.Installer.Win/Agent.Installer.Win.csproj +++ b/Agent.Installer.Win/Agent.Installer.Win.csproj @@ -15,6 +15,8 @@ true true + 10 + enable win;win-x64;win10-x64;win-x64;win10-x86; diff --git a/Agent.Installer.Win/App.xaml.cs b/Agent.Installer.Win/App.xaml.cs index 7a71daee4..82a1a9d36 100644 --- a/Agent.Installer.Win/App.xaml.cs +++ b/Agent.Installer.Win/App.xaml.cs @@ -1,12 +1,11 @@ using System.Windows; -namespace Remotely.Agent.Installer.Win +namespace Remotely.Agent.Installer.Win; + +/// +/// Interaction logic for App.xaml +/// +public partial class App : Application { - /// - /// Interaction logic for App.xaml - /// - public partial class App : Application - { - } } diff --git a/Agent.Installer.Win/MainWindow.xaml.cs b/Agent.Installer.Win/MainWindow.xaml.cs index 665e15ba5..9176cd2c5 100644 --- a/Agent.Installer.Win/MainWindow.xaml.cs +++ b/Agent.Installer.Win/MainWindow.xaml.cs @@ -5,70 +5,72 @@ using System.Windows.Documents; using System.Windows.Input; -namespace Remotely.Agent.Installer.Win +namespace Remotely.Agent.Installer.Win; + +/// +/// Interaction logic for MainWindow.xaml +/// +public partial class MainWindow : Window { - /// - /// Interaction logic for MainWindow.xaml - /// - public partial class MainWindow : Window + public MainWindow() { - public MainWindow() + if (CommandLineParser.CommandLineArgs.ContainsKey("quiet")) { - if (CommandLineParser.CommandLineArgs.ContainsKey("quiet")) - { - Hide(); - ShowInTaskbar = false; - _ = new MainWindowViewModel().Init(); - } - InitializeComponent(); + Hide(); + ShowInTaskbar = false; + _ = new MainWindowViewModel().Init(); } + InitializeComponent(); + } - private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) - { - DragMove(); - } + private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + DragMove(); + } - private async void Window_Loaded(object sender, RoutedEventArgs e) + private async void Window_Loaded(object sender, RoutedEventArgs e) + { + if (DataContext is MainWindowViewModel viewModel) { - await (DataContext as MainWindowViewModel).Init(); + await viewModel.Init(); } + } - private void CloseButton_Click(object sender, RoutedEventArgs e) - { - App.Current.Shutdown(); - } + private void CloseButton_Click(object sender, RoutedEventArgs e) + { + App.Current.Shutdown(); + } - private void MinimizeButton_Click(object sender, RoutedEventArgs e) - { - this.WindowState = WindowState.Minimized; - } + private void MinimizeButton_Click(object sender, RoutedEventArgs e) + { + this.WindowState = WindowState.Minimized; + } - private void ShowServerUrlHelp(object sender, RoutedEventArgs e) - { - MessageBox.Show( - "This is the URL of the Remotely server that you're hosting. The device will connect to this URL.", - "Server URL", - MessageBoxButton.OK, - MessageBoxImage.Information); - } + private void ShowServerUrlHelp(object sender, RoutedEventArgs e) + { + MessageBox.Show( + "This is the URL of the Remotely server that you're hosting. The device will connect to this URL.", + "Server URL", + MessageBoxButton.OK, + MessageBoxImage.Information); + } - private void ShowOrganizationIdHelp(object sender, RoutedEventArgs e) - { - MessageBox.Show( - "This is your organization ID on the Remotely server. Since Remotely supports multi-tenancy, " + - "this ID needs to be provided to determine who should have access." - + Environment.NewLine + Environment.NewLine + - "You can find this ID on the Organization tab on the web app.", - "Organization ID", - MessageBoxButton.OK, - MessageBoxImage.Information); - } - private void ShowSupportShortcutHelp(object sender, RoutedEventArgs e) - { - MessageBox.Show("If selected, the installer will create a desktop shortcut to the Get Support page for this device.", - "Support Shortcut", - MessageBoxButton.OK, - MessageBoxImage.Information); - } + private void ShowOrganizationIdHelp(object sender, RoutedEventArgs e) + { + MessageBox.Show( + "This is your organization ID on the Remotely server. Since Remotely supports multi-tenancy, " + + "this ID needs to be provided to determine who should have access." + + Environment.NewLine + Environment.NewLine + + "You can find this ID on the Organization tab on the web app.", + "Organization ID", + MessageBoxButton.OK, + MessageBoxImage.Information); + } + private void ShowSupportShortcutHelp(object sender, RoutedEventArgs e) + { + MessageBox.Show("If selected, the installer will create a desktop shortcut to the Get Support page for this device.", + "Support Shortcut", + MessageBoxButton.OK, + MessageBoxImage.Information); } } diff --git a/Agent.Installer.Win/Models/BrandingInfo.cs b/Agent.Installer.Win/Models/BrandingInfo.cs index 5e9a24688..c10237f6d 100644 --- a/Agent.Installer.Win/Models/BrandingInfo.cs +++ b/Agent.Installer.Win/Models/BrandingInfo.cs @@ -1,33 +1,28 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +#nullable enable -namespace Remotely.Agent.Installer.Win.Models +namespace Remotely.Agent.Installer.Win.Models; + +public class BrandingInfo { - public class BrandingInfo - { - public string Product { get; set; } = "Remotely"; + public string Product { get; set; } = "Remotely"; - public string Icon { get; set; } + public string? Icon { get; set; } - public byte TitleForegroundRed { get; set; } = 29; + public byte TitleForegroundRed { get; set; } = 29; - public byte TitleForegroundGreen { get; set; } = 144; + public byte TitleForegroundGreen { get; set; } = 144; - public byte TitleForegroundBlue { get; set; } = 241; + public byte TitleForegroundBlue { get; set; } = 241; - public byte TitleBackgroundRed { get; set; } = 70; + public byte TitleBackgroundRed { get; set; } = 70; - public byte TitleBackgroundGreen { get; set; } = 70; + public byte TitleBackgroundGreen { get; set; } = 70; - public byte TitleBackgroundBlue { get; set; } = 70; + public byte TitleBackgroundBlue { get; set; } = 70; - public byte ButtonForegroundRed { get; set; } = 255; + public byte ButtonForegroundRed { get; set; } = 255; - public byte ButtonForegroundGreen { get; set; } = 255; + public byte ButtonForegroundGreen { get; set; } = 255; - public byte ButtonForegroundBlue { get; set; } = 255; - } + public byte ButtonForegroundBlue { get; set; } = 255; } diff --git a/Agent.Installer.Win/Models/EmbeddedServerData.cs b/Agent.Installer.Win/Models/EmbeddedServerData.cs index 66bdbe1f6..dd096e6d3 100644 --- a/Agent.Installer.Win/Models/EmbeddedServerData.cs +++ b/Agent.Installer.Win/Models/EmbeddedServerData.cs @@ -1,33 +1,27 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; -using System.Runtime.Serialization.Formatters.Binary; -using System.Text; -using System.Threading.Tasks; -namespace Remotely.Agent.Installer.Models +namespace Remotely.Agent.Installer.Models; + +[DataContract] +public class EmbeddedServerData { - [DataContract] - public class EmbeddedServerData - { - /// - /// Parameterless constructor for JsonSerializer. - /// - public EmbeddedServerData() { } + /// + /// Parameterless constructor for JsonSerializer. + /// + public EmbeddedServerData() { } - public EmbeddedServerData(Uri serverUrl, string organizationId) - { - ServerUrl = serverUrl; - OrganizationId = organizationId; - } + public EmbeddedServerData(Uri serverUrl, string organizationId) + { + ServerUrl = serverUrl; + OrganizationId = organizationId; + } - public static EmbeddedServerData Empty { get; } = new EmbeddedServerData(); + public static EmbeddedServerData Empty { get; } = new EmbeddedServerData(); - [DataMember] - public string OrganizationId { get; set; } = string.Empty; + [DataMember] + public string OrganizationId { get; set; } = string.Empty; - [DataMember] - public Uri ServerUrl { get; set; } - } + [DataMember] + public Uri ServerUrl { get; set; } } diff --git a/Agent.Installer.Win/Services/EmbeddedServerDataReader.cs b/Agent.Installer.Win/Services/EmbeddedServerDataReader.cs index 21a5cb452..fd2825aa3 100644 --- a/Agent.Installer.Win/Services/EmbeddedServerDataReader.cs +++ b/Agent.Installer.Win/Services/EmbeddedServerDataReader.cs @@ -10,76 +10,75 @@ using Remotely.Agent.Installer.Win.Utilities; using Remotely.Shared; -namespace Remotely.Agent.Installer.Win.Services +namespace Remotely.Agent.Installer.Win.Services; + +internal class EmbeddedServerDataReader { - internal class EmbeddedServerDataReader - { - private readonly JavaScriptSerializer _serializer = new JavaScriptSerializer(); + private readonly JavaScriptSerializer _serializer = new JavaScriptSerializer(); - public Task TryGetEmbeddedData(string filePath) + public Task TryGetEmbeddedData(string filePath) + { + try { - try + if (!File.Exists(filePath)) { - if (!File.Exists(filePath)) + throw new Exception($"File path does not exist: {filePath}"); + } + + using (var fs = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + var result = SearchBuffer(fs, AppConstants.EmbeddedImmySignature); + if (result == -1) { - throw new Exception($"File path does not exist: {filePath}"); + throw new Exception("Signature not found in file buffer."); } - using (var fs = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) + Logger.Write($"Found data signature at index {result}."); + + fs.Seek(result + AppConstants.EmbeddedImmySignature.Length, SeekOrigin.Begin); + using (var reader = new BinaryReader(fs, Encoding.UTF8)) { - var result = SearchBuffer(fs, AppConstants.EmbeddedImmySignature); - if (result == -1) - { - throw new Exception("Signature not found in file buffer."); - } + var serializedData = reader.ReadString(); - Logger.Write($"Found data signature at index {result}."); + Logger.Write($"Extracted embedded data from EXE: {serializedData}"); - fs.Seek(result + AppConstants.EmbeddedImmySignature.Length, SeekOrigin.Begin); - using (var reader = new BinaryReader(fs, Encoding.UTF8)) + var embeddedData = _serializer.Deserialize(serializedData); + if (embeddedData != null) { - var serializedData = reader.ReadString(); - - Logger.Write($"Extracted embedded data from EXE: {serializedData}"); - - var embeddedData = _serializer.Deserialize(serializedData); - if (embeddedData != null) - { - return Task.FromResult(embeddedData); - } + return Task.FromResult(embeddedData); } } } - catch (Exception ex) - { - Logger.Write(ex); - } - return Task.FromResult(EmbeddedServerData.Empty); } + catch (Exception ex) + { + Logger.Write(ex); + } + return Task.FromResult(EmbeddedServerData.Empty); + } - private long SearchBuffer(FileStream fileStream, byte[] matchPattern) + private long SearchBuffer(FileStream fileStream, byte[] matchPattern) + { + var matchSize = matchPattern.Length; + var limit = fileStream.Length - matchSize; + + for (var i = 0; i <= limit; i++) { - var matchSize = matchPattern.Length; - var limit = fileStream.Length - matchSize; + var k = 0; - for (var i = 0; i <= limit; i++) + for (; k < matchSize; k++) { - var k = 0; - - for (; k < matchSize; k++) + if (matchPattern[k] != fileStream.ReadByte()) { - if (matchPattern[k] != fileStream.ReadByte()) - { - break; - } + break; } + } - if (k == matchSize) - { - return fileStream.Position - matchSize; - } + if (k == matchSize) + { + return fileStream.Position - matchSize; } - return -1; } + return -1; } } diff --git a/Agent.Installer.Win/Services/InstallerService.cs b/Agent.Installer.Win/Services/InstallerService.cs index 7c3c13595..110587166 100644 --- a/Agent.Installer.Win/Services/InstallerService.cs +++ b/Agent.Installer.Win/Services/InstallerService.cs @@ -18,437 +18,436 @@ using System.Windows; using FileIO = System.IO.File; -namespace Remotely.Agent.Installer.Win.Services +namespace Remotely.Agent.Installer.Win.Services; + +public class InstallerService { - public class InstallerService + private readonly string _installPath = Path.Combine(Path.GetPathRoot(Environment.SystemDirectory), "Program Files", "Remotely"); + private readonly string _platform = Environment.Is64BitOperatingSystem ? "x64" : "x86"; + private readonly JavaScriptSerializer _serializer = new JavaScriptSerializer(); + + public event EventHandler ProgressMessageChanged; + public event EventHandler ProgressValueChanged; + + public async Task Install(string serverUrl, + string organizationId, + string deviceGroup, + string deviceAlias, + string deviceUuid, + bool createSupportShortcut) { - private readonly string _installPath = Path.Combine(Path.GetPathRoot(Environment.SystemDirectory), "Program Files", "Remotely"); - private readonly string _platform = Environment.Is64BitOperatingSystem ? "x64" : "x86"; - private readonly JavaScriptSerializer _serializer = new JavaScriptSerializer(); - - public event EventHandler ProgressMessageChanged; - public event EventHandler ProgressValueChanged; - - public async Task Install(string serverUrl, - string organizationId, - string deviceGroup, - string deviceAlias, - string deviceUuid, - bool createSupportShortcut) + try { - try + Logger.Write("Install started."); + if (!CheckIsAdministrator()) { - Logger.Write("Install started."); - if (!CheckIsAdministrator()) - { - return false; - } + return false; + } - StopService(); + StopService(); - await StopProcesses(); + await StopProcesses(); - BackupDirectory(); + BackupDirectory(); - var connectionInfo = GetConnectionInfo(organizationId, serverUrl, deviceUuid); + var connectionInfo = GetConnectionInfo(organizationId, serverUrl, deviceUuid); - ClearInstallDirectory(); + ClearInstallDirectory(); - await DownloadRemotelyAgent(serverUrl); + await DownloadRemotelyAgent(serverUrl); - FileIO.WriteAllText(Path.Combine(_installPath, "ConnectionInfo.json"), _serializer.Serialize(connectionInfo)); + FileIO.WriteAllText(Path.Combine(_installPath, "ConnectionInfo.json"), _serializer.Serialize(connectionInfo)); - FileIO.Copy(Assembly.GetExecutingAssembly().Location, Path.Combine(_installPath, "Remotely_Installer.exe")); + FileIO.Copy(Assembly.GetExecutingAssembly().Location, Path.Combine(_installPath, "Remotely_Installer.exe")); - await CreateDeviceOnServer(connectionInfo.DeviceID, serverUrl, deviceGroup, deviceAlias, organizationId); + await CreateDeviceOnServer(connectionInfo.DeviceID, serverUrl, deviceGroup, deviceAlias, organizationId); - AddFirewallRule(); + AddFirewallRule(); - InstallService(); + InstallService(); - CreateUninstallKey(); + CreateUninstallKey(); - CreateSupportShortcut(serverUrl, connectionInfo.DeviceID, createSupportShortcut); - - return true; - } - catch (Exception ex) - { - Logger.Write(ex); - RestoreBackup(); - return false; - } + CreateSupportShortcut(serverUrl, connectionInfo.DeviceID, createSupportShortcut); + return true; + } + catch (Exception ex) + { + Logger.Write(ex); + RestoreBackup(); + return false; } - public async Task Uninstall() + } + + public async Task Uninstall() + { + try { - try + if (!CheckIsAdministrator()) { - if (!CheckIsAdministrator()) - { - return false; - } + return false; + } - StopService(); + StopService(); - ProcessEx.StartHidden("cmd.exe", "/c sc delete Remotely_Service").WaitForExit(); + ProcessEx.StartHidden("cmd.exe", "/c sc delete Remotely_Service").WaitForExit(); - await StopProcesses(); + await StopProcesses(); - ProgressMessageChanged?.Invoke(this, "Deleting files."); - ClearInstallDirectory(); - ProcessEx.StartHidden("cmd.exe", $"/c timeout 5 & rd /s /q \"{_installPath}\""); + ProgressMessageChanged?.Invoke(this, "Deleting files."); + ClearInstallDirectory(); + ProcessEx.StartHidden("cmd.exe", $"/c timeout 5 & rd /s /q \"{_installPath}\""); - ProcessEx.StartHidden("netsh", "advfirewall firewall delete rule name=\"Remotely Desktop Unattended\"").WaitForExit(); + ProcessEx.StartHidden("netsh", "advfirewall firewall delete rule name=\"Remotely Desktop Unattended\"").WaitForExit(); - GetRegistryBaseKey().DeleteSubKeyTree(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Remotely", false); + GetRegistryBaseKey().DeleteSubKeyTree(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Remotely", false); - return true; - } - catch (Exception ex) - { - Logger.Write(ex); - return false; - } + return true; } - - private void AddFirewallRule() + catch (Exception ex) { - var desktopExePath = Path.Combine(_installPath, "Desktop", "Remotely_Desktop.exe"); - ProcessEx.StartHidden("netsh", "advfirewall firewall delete rule name=\"Remotely Desktop Unattended\"").WaitForExit(); - ProcessEx.StartHidden("netsh", $"advfirewall firewall add rule name=\"Remotely Desktop Unattended\" program=\"{desktopExePath}\" protocol=any dir=in enable=yes action=allow description=\"The agent that allows screen sharing and remote control for Remotely.\"").WaitForExit(); + Logger.Write(ex); + return false; } + } - private void BackupDirectory() + private void AddFirewallRule() + { + var desktopExePath = Path.Combine(_installPath, "Desktop", "Remotely_Desktop.exe"); + ProcessEx.StartHidden("netsh", "advfirewall firewall delete rule name=\"Remotely Desktop Unattended\"").WaitForExit(); + ProcessEx.StartHidden("netsh", $"advfirewall firewall add rule name=\"Remotely Desktop Unattended\" program=\"{desktopExePath}\" protocol=any dir=in enable=yes action=allow description=\"The agent that allows screen sharing and remote control for Remotely.\"").WaitForExit(); + } + + private void BackupDirectory() + { + if (Directory.Exists(_installPath)) { - if (Directory.Exists(_installPath)) + Logger.Write("Backing up current installation."); + ProgressMessageChanged?.Invoke(this, "Backing up current installation."); + var backupPath = Path.Combine(Path.GetTempPath(), "Remotely_Backup.zip"); + if (FileIO.Exists(backupPath)) { - Logger.Write("Backing up current installation."); - ProgressMessageChanged?.Invoke(this, "Backing up current installation."); - var backupPath = Path.Combine(Path.GetTempPath(), "Remotely_Backup.zip"); - if (FileIO.Exists(backupPath)) - { - FileIO.Delete(backupPath); - } - ZipFile.CreateFromDirectory(_installPath, backupPath, CompressionLevel.Fastest, false); + FileIO.Delete(backupPath); } + ZipFile.CreateFromDirectory(_installPath, backupPath, CompressionLevel.Fastest, false); } + } - private bool CheckIsAdministrator() + private bool CheckIsAdministrator() + { + var identity = WindowsIdentity.GetCurrent(); + var principal = new WindowsPrincipal(identity); + var result = principal.IsInRole(WindowsBuiltInRole.Administrator); + if (!result) { - var identity = WindowsIdentity.GetCurrent(); - var principal = new WindowsPrincipal(identity); - var result = principal.IsInRole(WindowsBuiltInRole.Administrator); - if (!result) - { - MessageBoxEx.Show("Elevated privileges are required. Please restart the installer using 'Run as administrator'.", "Elevation Required", MessageBoxButton.OK, MessageBoxImage.Warning); - } - return result; + MessageBoxEx.Show("Elevated privileges are required. Please restart the installer using 'Run as administrator'.", "Elevation Required", MessageBoxButton.OK, MessageBoxImage.Warning); } + return result; + } - private void ClearInstallDirectory() + private void ClearInstallDirectory() + { + if (Directory.Exists(_installPath)) { - if (Directory.Exists(_installPath)) + foreach (var entry in Directory.GetFileSystemEntries(_installPath)) { - foreach (var entry in Directory.GetFileSystemEntries(_installPath)) + try { - try + if (FileIO.Exists(entry)) { - if (FileIO.Exists(entry)) - { - FileIO.Delete(entry); - } - else if (Directory.Exists(entry)) - { - Directory.Delete(entry, true); - } + FileIO.Delete(entry); } - catch (Exception ex) + else if (Directory.Exists(entry)) { - Logger.Write(ex); + Directory.Delete(entry, true); } } + catch (Exception ex) + { + Logger.Write(ex); + } } } + } - private async Task CreateDeviceOnServer(string deviceUuid, - string serverUrl, - string deviceGroup, - string deviceAlias, - string organizationId) + private async Task CreateDeviceOnServer(string deviceUuid, + string serverUrl, + string deviceGroup, + string deviceAlias, + string organizationId) + { + try { - try + if (!string.IsNullOrWhiteSpace(deviceGroup) || + !string.IsNullOrWhiteSpace(deviceAlias)) { - if (!string.IsNullOrWhiteSpace(deviceGroup) || - !string.IsNullOrWhiteSpace(deviceAlias)) + var setupOptions = new DeviceSetupOptions() { - var setupOptions = new DeviceSetupOptions() - { - DeviceID = deviceUuid, - DeviceGroupName = deviceGroup, - DeviceAlias = deviceAlias, - OrganizationID = organizationId - }; - - var wr = WebRequest.CreateHttp(serverUrl.TrimEnd('/') + "/api/devices"); - wr.Method = "POST"; - wr.ContentType = "application/json"; - using (var rs = await wr.GetRequestStreamAsync()) - using (var sw = new StreamWriter(rs)) - { - await sw.WriteAsync(_serializer.Serialize(setupOptions)); - } - using (var response = await wr.GetResponseAsync() as HttpWebResponse) - { - Logger.Write($"Create device response: {response.StatusCode}"); - } + DeviceID = deviceUuid, + DeviceGroupName = deviceGroup, + DeviceAlias = deviceAlias, + OrganizationID = organizationId + }; + + var wr = WebRequest.CreateHttp(serverUrl.TrimEnd('/') + "/api/devices"); + wr.Method = "POST"; + wr.ContentType = "application/json"; + using (var rs = await wr.GetRequestStreamAsync()) + using (var sw = new StreamWriter(rs)) + { + await sw.WriteAsync(_serializer.Serialize(setupOptions)); + } + using (var response = await wr.GetResponseAsync() as HttpWebResponse) + { + Logger.Write($"Create device response: {response.StatusCode}"); } } - catch (WebException ex) when ((ex.Response is HttpWebResponse response) && response.StatusCode == HttpStatusCode.BadRequest) - { - Logger.Write("Bad request when creating device. The device ID may already be created."); - } - catch (Exception ex) - { - Logger.Write(ex); - } - } - - private void CreateSupportShortcut(string serverUrl, string deviceUuid, bool createSupportShortcut) + catch (WebException ex) when ((ex.Response is HttpWebResponse response) && response.StatusCode == HttpStatusCode.BadRequest) { - var shell = new WshShell(); - var shortcutLocation = Path.Combine(_installPath, "Get Support.lnk"); - var shortcut = (IWshShortcut)shell.CreateShortcut(shortcutLocation); - shortcut.Description = "Get IT support"; - shortcut.IconLocation = Path.Combine(_installPath, "Remotely_Agent.exe"); - shortcut.TargetPath = serverUrl.TrimEnd('/') + $"/GetSupport?deviceID={deviceUuid}"; - shortcut.Save(); - - if (createSupportShortcut) - { - var systemRoot = Path.GetPathRoot(Environment.SystemDirectory); - var publicDesktop = Path.Combine(systemRoot, "Users", "Public", "Desktop", "Get Support.lnk"); - FileIO.Copy(shortcutLocation, publicDesktop, true); - } + Logger.Write("Bad request when creating device. The device ID may already be created."); } - private void CreateUninstallKey() + catch (Exception ex) { - var version = FileVersionInfo.GetVersionInfo(Path.Combine(_installPath, "Remotely_Agent.exe")); - var baseKey = GetRegistryBaseKey(); - - var remotelyKey = baseKey.CreateSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Remotely", true); - remotelyKey.SetValue("DisplayIcon", Path.Combine(_installPath, "Remotely_Agent.exe")); - remotelyKey.SetValue("DisplayName", "Remotely"); - remotelyKey.SetValue("DisplayVersion", version.FileVersion); - remotelyKey.SetValue("InstallDate", DateTime.Now.ToShortDateString()); - remotelyKey.SetValue("Publisher", "Immense Networks"); - remotelyKey.SetValue("VersionMajor", version.FileMajorPart.ToString(), RegistryValueKind.DWord); - remotelyKey.SetValue("VersionMinor", version.FileMinorPart.ToString(), RegistryValueKind.DWord); - remotelyKey.SetValue("UninstallString", Path.Combine(_installPath, "Remotely_Installer.exe -uninstall -quiet")); - remotelyKey.SetValue("QuietUninstallString", Path.Combine(_installPath, "Remotely_Installer.exe -uninstall -quiet")); + Logger.Write(ex); } - private async Task DownloadRemotelyAgent(string serverUrl) + } + + private void CreateSupportShortcut(string serverUrl, string deviceUuid, bool createSupportShortcut) + { + var shell = new WshShell(); + var shortcutLocation = Path.Combine(_installPath, "Get Support.lnk"); + var shortcut = (IWshShortcut)shell.CreateShortcut(shortcutLocation); + shortcut.Description = "Get IT support"; + shortcut.IconLocation = Path.Combine(_installPath, "Remotely_Agent.exe"); + shortcut.TargetPath = serverUrl.TrimEnd('/') + $"/GetSupport?deviceID={deviceUuid}"; + shortcut.Save(); + + if (createSupportShortcut) { - var targetFile = Path.Combine(Path.GetTempPath(), $"Remotely-Agent.zip"); + var systemRoot = Path.GetPathRoot(Environment.SystemDirectory); + var publicDesktop = Path.Combine(systemRoot, "Users", "Public", "Desktop", "Get Support.lnk"); + FileIO.Copy(shortcutLocation, publicDesktop, true); + } + } + private void CreateUninstallKey() + { + var version = FileVersionInfo.GetVersionInfo(Path.Combine(_installPath, "Remotely_Agent.exe")); + var baseKey = GetRegistryBaseKey(); + + var remotelyKey = baseKey.CreateSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Remotely", true); + remotelyKey.SetValue("DisplayIcon", Path.Combine(_installPath, "Remotely_Agent.exe")); + remotelyKey.SetValue("DisplayName", "Remotely"); + remotelyKey.SetValue("DisplayVersion", version.FileVersion); + remotelyKey.SetValue("InstallDate", DateTime.Now.ToShortDateString()); + remotelyKey.SetValue("Publisher", "Immense Networks"); + remotelyKey.SetValue("VersionMajor", version.FileMajorPart.ToString(), RegistryValueKind.DWord); + remotelyKey.SetValue("VersionMinor", version.FileMinorPart.ToString(), RegistryValueKind.DWord); + remotelyKey.SetValue("UninstallString", Path.Combine(_installPath, "Remotely_Installer.exe -uninstall -quiet")); + remotelyKey.SetValue("QuietUninstallString", Path.Combine(_installPath, "Remotely_Installer.exe -uninstall -quiet")); + } - if (CommandLineParser.CommandLineArgs.TryGetValue("path", out var result) && - FileIO.Exists(result)) - { - targetFile = result; - } - else + private async Task DownloadRemotelyAgent(string serverUrl) + { + var targetFile = Path.Combine(Path.GetTempPath(), $"Remotely-Agent.zip"); + + if (CommandLineParser.CommandLineArgs.TryGetValue("path", out var result) && + FileIO.Exists(result)) + { + targetFile = result; + } + else + { + ProgressMessageChanged.Invoke(this, "Downloading Remotely agent."); + using (var client = new WebClient()) { - ProgressMessageChanged.Invoke(this, "Downloading Remotely agent."); - using (var client = new WebClient()) + client.DownloadProgressChanged += (sender, args) => { - client.DownloadProgressChanged += (sender, args) => - { - ProgressValueChanged?.Invoke(this, args.ProgressPercentage); - }; + ProgressValueChanged?.Invoke(this, args.ProgressPercentage); + }; - await client.DownloadFileTaskAsync($"{serverUrl}/Content/Remotely-Win10-{_platform}.zip", targetFile); - } + await client.DownloadFileTaskAsync($"{serverUrl}/Content/Remotely-Win10-{_platform}.zip", targetFile); } + } - ProgressMessageChanged.Invoke(this, "Extracting Remotely files."); - ProgressValueChanged?.Invoke(this, 0); + ProgressMessageChanged.Invoke(this, "Extracting Remotely files."); + ProgressValueChanged?.Invoke(this, 0); - var tempDir = Path.Combine(Path.GetTempPath(), "RemotelyUpdate"); - if (Directory.Exists(tempDir)) - { - Directory.Delete(tempDir, true); - } + var tempDir = Path.Combine(Path.GetTempPath(), "RemotelyUpdate"); + if (Directory.Exists(tempDir)) + { + Directory.Delete(tempDir, true); + } - Directory.CreateDirectory(_installPath); - while (!Directory.Exists(_installPath)) - { - await Task.Delay(10); - } + Directory.CreateDirectory(_installPath); + while (!Directory.Exists(_installPath)) + { + await Task.Delay(10); + } - var wr = WebRequest.CreateHttp($"{serverUrl}/Content/Remotely-Win10-{_platform}.zip"); - wr.Method = "Head"; - using (var response = (HttpWebResponse)await wr.GetResponseAsync()) - { - FileIO.WriteAllText(Path.Combine(_installPath, "etag.txt"), response.Headers["ETag"]); - } + var wr = WebRequest.CreateHttp($"{serverUrl}/Content/Remotely-Win10-{_platform}.zip"); + wr.Method = "Head"; + using (var response = (HttpWebResponse)await wr.GetResponseAsync()) + { + FileIO.WriteAllText(Path.Combine(_installPath, "etag.txt"), response.Headers["ETag"]); + } - ZipFile.ExtractToDirectory(targetFile, tempDir); - var fileSystemEntries = Directory.GetFileSystemEntries(tempDir); - for (var i = 0; i < fileSystemEntries.Length; i++) + ZipFile.ExtractToDirectory(targetFile, tempDir); + var fileSystemEntries = Directory.GetFileSystemEntries(tempDir); + for (var i = 0; i < fileSystemEntries.Length; i++) + { + try { - try + ProgressValueChanged?.Invoke(this, (int)((double)i / (double)fileSystemEntries.Length * 100d)); + var entry = fileSystemEntries[i]; + if (FileIO.Exists(entry)) { - ProgressValueChanged?.Invoke(this, (int)((double)i / (double)fileSystemEntries.Length * 100d)); - var entry = fileSystemEntries[i]; - if (FileIO.Exists(entry)) - { - FileIO.Copy(entry, Path.Combine(_installPath, Path.GetFileName(entry)), true); - } - else if (Directory.Exists(entry)) - { - FileSystem.CopyDirectory(entry, Path.Combine(_installPath, new DirectoryInfo(entry).Name), true); - } - await Task.Delay(1); + FileIO.Copy(entry, Path.Combine(_installPath, Path.GetFileName(entry)), true); } - catch (Exception ex) + else if (Directory.Exists(entry)) { - Logger.Write(ex); + FileSystem.CopyDirectory(entry, Path.Combine(_installPath, new DirectoryInfo(entry).Name), true); } + await Task.Delay(1); } - ProgressValueChanged?.Invoke(this, 0); - } - - private ConnectionInfo GetConnectionInfo(string organizationId, string serverUrl, string deviceUuid) - { - ConnectionInfo connectionInfo; - var connectionInfoPath = Path.Combine(_installPath, "ConnectionInfo.json"); - if (FileIO.Exists(connectionInfoPath)) - { - connectionInfo = _serializer.Deserialize(FileIO.ReadAllText(connectionInfoPath)); - connectionInfo.ServerVerificationToken = null; - } - else + catch (Exception ex) { - connectionInfo = new ConnectionInfo() - { - DeviceID = Guid.NewGuid().ToString() - }; + Logger.Write(ex); } + } + ProgressValueChanged?.Invoke(this, 0); + } - if (!string.IsNullOrWhiteSpace(deviceUuid)) + private ConnectionInfo GetConnectionInfo(string organizationId, string serverUrl, string deviceUuid) + { + ConnectionInfo connectionInfo; + var connectionInfoPath = Path.Combine(_installPath, "ConnectionInfo.json"); + if (FileIO.Exists(connectionInfoPath)) + { + connectionInfo = _serializer.Deserialize(FileIO.ReadAllText(connectionInfoPath)); + connectionInfo.ServerVerificationToken = null; + } + else + { + connectionInfo = new ConnectionInfo() { - // Clear the server verification token if we're installing this as a new device. - if (connectionInfo.DeviceID != deviceUuid) - { - connectionInfo.ServerVerificationToken = null; - } - connectionInfo.DeviceID = deviceUuid; - } - connectionInfo.OrganizationID = organizationId; - connectionInfo.Host = serverUrl; - return connectionInfo; + DeviceID = Guid.NewGuid().ToString() + }; } - private RegistryKey GetRegistryBaseKey() + if (!string.IsNullOrWhiteSpace(deviceUuid)) { - if (Environment.Is64BitOperatingSystem) + // Clear the server verification token if we're installing this as a new device. + if (connectionInfo.DeviceID != deviceUuid) { - return RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64); - } - else - { - return RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32); + connectionInfo.ServerVerificationToken = null; } + connectionInfo.DeviceID = deviceUuid; } + connectionInfo.OrganizationID = organizationId; + connectionInfo.Host = serverUrl; + return connectionInfo; + } - private void InstallService() + private RegistryKey GetRegistryBaseKey() + { + if (Environment.Is64BitOperatingSystem) { - Logger.Write("Installing service."); - ProgressMessageChanged?.Invoke(this, "Installing Remotely service."); - var serv = ServiceController.GetServices().FirstOrDefault(ser => ser.ServiceName == "Remotely_Service"); - if (serv == null) - { - var command = new string[] { "/assemblypath=" + Path.Combine(_installPath, "Remotely_Agent.exe") }; - var context = new InstallContext("", command); - var serviceInstaller = new ServiceInstaller() - { - Context = context, - DisplayName = "Remotely Service", - Description = "Background service that maintains a connection to the Remotely server. The service is used for remote support and maintenance by this computer's administrators.", - ServiceName = "Remotely_Service", - StartType = ServiceStartMode.Automatic, - Parent = new ServiceProcessInstaller() - }; - - var state = new System.Collections.Specialized.ListDictionary(); - serviceInstaller.Install(state); - Logger.Write("Service installed."); - serv = ServiceController.GetServices().FirstOrDefault(ser => ser.ServiceName == "Remotely_Service"); + return RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64); + } + else + { + return RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32); + } + } - ProcessEx.StartHidden("cmd.exe", "/c sc.exe failure \"Remotely_Service\" reset= 5 actions= restart/5000"); - } - if (serv.Status != ServiceControllerStatus.Running) + private void InstallService() + { + Logger.Write("Installing service."); + ProgressMessageChanged?.Invoke(this, "Installing Remotely service."); + var serv = ServiceController.GetServices().FirstOrDefault(ser => ser.ServiceName == "Remotely_Service"); + if (serv == null) + { + var command = new string[] { "/assemblypath=" + Path.Combine(_installPath, "Remotely_Agent.exe") }; + var context = new InstallContext("", command); + var serviceInstaller = new ServiceInstaller() { - serv.Start(); - } - Logger.Write("Service started."); + Context = context, + DisplayName = "Remotely Service", + Description = "Background service that maintains a connection to the Remotely server. The service is used for remote support and maintenance by this computer's administrators.", + ServiceName = "Remotely_Service", + StartType = ServiceStartMode.Automatic, + Parent = new ServiceProcessInstaller() + }; + + var state = new System.Collections.Specialized.ListDictionary(); + serviceInstaller.Install(state); + Logger.Write("Service installed."); + serv = ServiceController.GetServices().FirstOrDefault(ser => ser.ServiceName == "Remotely_Service"); + + ProcessEx.StartHidden("cmd.exe", "/c sc.exe failure \"Remotely_Service\" reset= 5 actions= restart/5000"); + } + if (serv.Status != ServiceControllerStatus.Running) + { + serv.Start(); } + Logger.Write("Service started."); + } - private void RestoreBackup() + private void RestoreBackup() + { + try { - try + var backupPath = Path.Combine(Path.GetTempPath(), "Remotely_Backup.zip"); + if (FileIO.Exists(backupPath)) { - var backupPath = Path.Combine(Path.GetTempPath(), "Remotely_Backup.zip"); - if (FileIO.Exists(backupPath)) + Logger.Write("Restoring backup."); + ClearInstallDirectory(); + ZipFile.ExtractToDirectory(backupPath, _installPath); + var serv = ServiceController.GetServices().FirstOrDefault(ser => ser.ServiceName == "Remotely_Service"); + if (serv?.Status != ServiceControllerStatus.Running) { - Logger.Write("Restoring backup."); - ClearInstallDirectory(); - ZipFile.ExtractToDirectory(backupPath, _installPath); - var serv = ServiceController.GetServices().FirstOrDefault(ser => ser.ServiceName == "Remotely_Service"); - if (serv?.Status != ServiceControllerStatus.Running) - { - serv?.Start(); - } + serv?.Start(); } } - catch (Exception ex) - { - Logger.Write(ex); - } } - - private async Task StopProcesses() + catch (Exception ex) { - ProgressMessageChanged?.Invoke(this, "Stopping Remotely processes."); - var procs = Process.GetProcessesByName("Remotely_Agent").Concat(Process.GetProcessesByName("Remotely_Desktop")); + Logger.Write(ex); + } + } - foreach (var proc in procs) - { - proc.Kill(); - } + private async Task StopProcesses() + { + ProgressMessageChanged?.Invoke(this, "Stopping Remotely processes."); + var procs = Process.GetProcessesByName("Remotely_Agent").Concat(Process.GetProcessesByName("Remotely_Desktop")); - await Task.Delay(500); + foreach (var proc in procs) + { + proc.Kill(); } - private void StopService() + + await Task.Delay(500); + } + private void StopService() + { + try { - try + var remotelyService = ServiceController.GetServices().FirstOrDefault(x => x.ServiceName == "Remotely_Service"); + if (remotelyService != null) { - var remotelyService = ServiceController.GetServices().FirstOrDefault(x => x.ServiceName == "Remotely_Service"); - if (remotelyService != null) - { - Logger.Write("Stopping existing Remotely service."); - ProgressMessageChanged?.Invoke(this, "Stopping existing Remotely service."); - remotelyService.Stop(); - remotelyService.WaitForStatus(ServiceControllerStatus.Stopped); - } - } - catch (Exception ex) - { - Logger.Write(ex); + Logger.Write("Stopping existing Remotely service."); + ProgressMessageChanged?.Invoke(this, "Stopping existing Remotely service."); + remotelyService.Stop(); + remotelyService.WaitForStatus(ServiceControllerStatus.Stopped); } } + catch (Exception ex) + { + Logger.Write(ex); + } } } diff --git a/Agent.Installer.Win/Services/RelayCommand.cs b/Agent.Installer.Win/Services/RelayCommand.cs index eb14f35d7..112042d79 100644 --- a/Agent.Installer.Win/Services/RelayCommand.cs +++ b/Agent.Installer.Win/Services/RelayCommand.cs @@ -1,38 +1,39 @@ -using System; +#nullable enable + +using System; using System.Windows.Input; -namespace Remotely.Agent.Installer.Win.Services +namespace Remotely.Agent.Installer.Win.Services; + +public class RelayCommand : ICommand { - public class RelayCommand : ICommand - { - private readonly Action _action; + private readonly Action _action; - private readonly Predicate _canExecute; + private readonly Predicate? _canExecute; - public RelayCommand(Action action, Predicate canExecute = null) - { - _action = action; - _canExecute = canExecute; - } + public RelayCommand(Action action, Predicate? canExecute = null) + { + _action = action; + _canExecute = canExecute; + } - public event EventHandler CanExecuteChanged - { - add { CommandManager.RequerySuggested += value; } - remove { CommandManager.RequerySuggested -= value; } - } + public event EventHandler CanExecuteChanged + { + add { CommandManager.RequerySuggested += value; } + remove { CommandManager.RequerySuggested -= value; } + } - public bool CanExecute(object parameter) + public bool CanExecute(object parameter) + { + if (_canExecute is null) { - if (_canExecute is null) - { - return true; - } - return _canExecute.Invoke(parameter); + return true; } + return _canExecute.Invoke(parameter); + } - public void Execute(object parameter) - { - _action?.Invoke(parameter); - } + public void Execute(object parameter) + { + _action?.Invoke(parameter); } } diff --git a/Agent.Installer.Win/Utilities/CommandLineParser.cs b/Agent.Installer.Win/Utilities/CommandLineParser.cs index 34ba427d3..a24e994d5 100644 --- a/Agent.Installer.Win/Utilities/CommandLineParser.cs +++ b/Agent.Installer.Win/Utilities/CommandLineParser.cs @@ -1,76 +1,78 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; using System.ComponentModel; using System.Windows; -namespace Remotely.Agent.Installer.Win.Utilities +namespace Remotely.Agent.Installer.Win.Utilities; + +public class CommandLineParser { - public class CommandLineParser - { - private static Dictionary commandLineArgs; - private static bool _invalidArgumentFound; + private static Dictionary? commandLineArgs; - public static Dictionary CommandLineArgs + private static bool _invalidArgumentFound; + + public static Dictionary CommandLineArgs + { + get { - get + if (commandLineArgs is null) { - if (commandLineArgs is null) - { - commandLineArgs = new Dictionary(); + commandLineArgs = new Dictionary(); + + var args = Environment.GetCommandLineArgs(); - var args = Environment.GetCommandLineArgs(); - for (var i = 1; i < args.Length; i += 2) + for (var i = 1; i < args.Length; i += 2) + { + try { - try + var key = args[i]; + if (key != null) { - var key = args?[i]; - if (key != null) + if (!key.Contains("-")) { - if (!key.Contains("-")) - { - _invalidArgumentFound = true; - i -= 1; - continue; - } - key = key.Trim().Replace("-", "").ToLower(); - if (i + 1 == args.Length) + _invalidArgumentFound = true; + i -= 1; + continue; + } + key = key.Trim().Replace("-", "").ToLower(); + if (i + 1 == args.Length) + { + commandLineArgs.Add(key, "true"); + continue; + } + var value = args[i + 1]; + if (value != null) + { + if (value.StartsWith("-")) { commandLineArgs.Add(key, "true"); - continue; + i -= 1; } - var value = args[i + 1]; - if (value != null) + else { - if (value.StartsWith("-")) - { - commandLineArgs.Add(key, "true"); - i -= 1; - } - else - { - commandLineArgs.Add(key, args[i + 1].Trim()); - } + commandLineArgs.Add(key, args[i + 1].Trim()); } } } - catch (Exception ex) - { - Logger.Write(ex); - } - } + catch (Exception ex) + { + Logger.Write(ex); + } + } - return commandLineArgs; } + return commandLineArgs; } + } - internal static void VerifyArguments() + internal static void VerifyArguments() + { + if (_invalidArgumentFound) { - if (_invalidArgumentFound) - { - Logger.Write("Command line arguments are invalid."); - MessageBoxEx.Show("Command line arguments are invalid.", "Invalid Arguments", MessageBoxButton.OK, MessageBoxImage.Error); - } + Logger.Write("Command line arguments are invalid."); + MessageBoxEx.Show("Command line arguments are invalid.", "Invalid Arguments", MessageBoxButton.OK, MessageBoxImage.Error); } } } diff --git a/Agent.Installer.Win/Utilities/Logger.cs b/Agent.Installer.Win/Utilities/Logger.cs index a68ae02c9..f1e16ce61 100644 --- a/Agent.Installer.Win/Utilities/Logger.cs +++ b/Agent.Installer.Win/Utilities/Logger.cs @@ -2,81 +2,80 @@ using System.IO; using System.Linq; -namespace Remotely.Agent.Installer.Win.Utilities +namespace Remotely.Agent.Installer.Win.Utilities; + +public class Logger { - public class Logger - { - public static string LogsDir { get; } = Directory.CreateDirectory(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Remotely", "Logs")).FullName; - public static string LogsPath { get; } = Path.Combine(LogsDir, "Installer.log"); + public static string LogsDir { get; } = Directory.CreateDirectory(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Remotely", "Logs")).FullName; + public static string LogsPath { get; } = Path.Combine(LogsDir, "Installer.log"); - private static readonly object _writeLock = new object(); + private static readonly object _writeLock = new object(); - public static void Debug(string message) + public static void Debug(string message) + { + lock (_writeLock) { - lock (_writeLock) - { #if DEBUG - CheckLogFileExists(); + CheckLogFileExists(); - File.AppendAllText(LogsPath, $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}\t[Debug]\t{message}{Environment.NewLine}"); + File.AppendAllText(LogsPath, $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}\t[Debug]\t{message}{Environment.NewLine}"); #endif - System.Diagnostics.Debug.WriteLine(message); - } - + System.Diagnostics.Debug.WriteLine(message); } - public static void Write(string message) + } + + public static void Write(string message) + { + try { - try + lock (_writeLock) { - lock (_writeLock) - { - CheckLogFileExists(); - File.AppendAllText(LogsPath, $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}\t[Info]\t{message}{Environment.NewLine}"); - Console.WriteLine(message); - } + CheckLogFileExists(); + File.AppendAllText(LogsPath, $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}\t[Info]\t{message}{Environment.NewLine}"); + Console.WriteLine(message); } - catch { } } + catch { } + } - public static void Write(Exception ex) + public static void Write(Exception ex) + { + lock (_writeLock) { - lock (_writeLock) + try { - try - { - CheckLogFileExists(); + CheckLogFileExists(); - var exception = ex; + var exception = ex; - while (exception != null) - { - File.AppendAllText(LogsPath, $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}\t[Error]\t{exception?.Message}\t{exception?.StackTrace}\t{exception?.Source}{Environment.NewLine}"); - Console.WriteLine(exception.Message); - exception = exception.InnerException; - } + while (exception != null) + { + File.AppendAllText(LogsPath, $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}\t[Error]\t{exception?.Message}\t{exception?.StackTrace}\t{exception?.Source}{Environment.NewLine}"); + Console.WriteLine(exception.Message); + exception = exception.InnerException; } - catch { } } + catch { } } + } - private static void CheckLogFileExists() + private static void CheckLogFileExists() + { + if (!File.Exists(LogsPath)) { - if (!File.Exists(LogsPath)) - { - File.Create(LogsPath).Close(); - } + File.Create(LogsPath).Close(); + } - if (File.Exists(LogsPath)) + if (File.Exists(LogsPath)) + { + var fi = new FileInfo(LogsPath); + while (fi.Length > 1000000) { - var fi = new FileInfo(LogsPath); - while (fi.Length > 1000000) - { - var content = File.ReadAllLines(LogsPath); - File.WriteAllLines(LogsPath, content.Skip(10)); - fi = new FileInfo(LogsPath); - } + var content = File.ReadAllLines(LogsPath); + File.WriteAllLines(LogsPath, content.Skip(10)); + fi = new FileInfo(LogsPath); } } } diff --git a/Agent.Installer.Win/Utilities/MessageBoxEx.cs b/Agent.Installer.Win/Utilities/MessageBoxEx.cs index 5e1ff155d..65ff2e8ae 100644 --- a/Agent.Installer.Win/Utilities/MessageBoxEx.cs +++ b/Agent.Installer.Win/Utilities/MessageBoxEx.cs @@ -1,16 +1,15 @@ using System.Windows; -namespace Remotely.Agent.Installer.Win.Utilities +namespace Remotely.Agent.Installer.Win.Utilities; + +public static class MessageBoxEx { - public static class MessageBoxEx + public static MessageBoxResult Show(string message, string caption, MessageBoxButton messageBoxButton, MessageBoxImage messageBoxImage) { - public static MessageBoxResult Show(string message, string caption, MessageBoxButton messageBoxButton, MessageBoxImage messageBoxImage) + if (!CommandLineParser.CommandLineArgs.ContainsKey("quiet")) { - if (!CommandLineParser.CommandLineArgs.ContainsKey("quiet")) - { - return MessageBox.Show(message, caption, messageBoxButton, messageBoxImage); - } - return MessageBoxResult.None; + return MessageBox.Show(message, caption, messageBoxButton, messageBoxImage); } + return MessageBoxResult.None; } } diff --git a/Agent.Installer.Win/Utilities/ProcessEx.cs b/Agent.Installer.Win/Utilities/ProcessEx.cs index f58f903d8..6515dfb6e 100644 --- a/Agent.Installer.Win/Utilities/ProcessEx.cs +++ b/Agent.Installer.Win/Utilities/ProcessEx.cs @@ -1,19 +1,18 @@ using System.Diagnostics; -namespace Remotely.Agent.Installer.Win.Utilities +namespace Remotely.Agent.Installer.Win.Utilities; + +public static class ProcessEx { - public static class ProcessEx + public static Process StartHidden(string filePath, string arguments) { - public static Process StartHidden(string filePath, string arguments) + var psi = new ProcessStartInfo() { - var psi = new ProcessStartInfo() - { - WindowStyle = ProcessWindowStyle.Hidden, - CreateNoWindow = true, - Arguments = arguments, - FileName = filePath - }; - return Process.Start(psi); - } + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = true, + Arguments = arguments, + FileName = filePath + }; + return Process.Start(psi); } } diff --git a/Agent.Installer.Win/ViewModels/MainWindowViewModel.cs b/Agent.Installer.Win/ViewModels/MainWindowViewModel.cs index eec29bbaa..94141a9c2 100644 --- a/Agent.Installer.Win/ViewModels/MainWindowViewModel.cs +++ b/Agent.Installer.Win/ViewModels/MainWindowViewModel.cs @@ -1,3 +1,4 @@ +#nullable enable using Remotely.Agent.Installer.Win.Models; using Remotely.Agent.Installer.Win.Services; using Remotely.Agent.Installer.Win.Utilities; @@ -20,507 +21,508 @@ using Remotely.Shared; using Remotely.Agent.Installer.Models; -namespace Remotely.Agent.Installer.Win.ViewModels +namespace Remotely.Agent.Installer.Win.ViewModels; + +public class MainWindowViewModel : ViewModelBase { - public class MainWindowViewModel : ViewModelBase - { - private readonly InstallerService _installer; - private readonly EmbeddedServerDataReader _embeddedDataReader; - private BrandingInfo _brandingInfo; - private bool _createSupportShortcut; - private string _headerMessage = "Install the service."; - private bool _isReadyState = true; - private bool _isServiceInstalled; + private readonly EmbeddedServerDataReader _embeddedDataReader; + private readonly InstallerService _installer; + private BrandingInfo? _brandingInfo; + private bool _createSupportShortcut; + private string _headerMessage = "Install the service."; + private bool _isReadyState = true; + private bool _isServiceInstalled; - private string _organizationID; + private string? _organizationID; - private int _progress; + private int _progress; - private string _serverUrl = string.Empty; + private string _serverUrl = string.Empty; - private string _statusMessage; - public MainWindowViewModel() - { - _installer = new InstallerService(); - _embeddedDataReader = new EmbeddedServerDataReader(); + private string? _statusMessage; + + public MainWindowViewModel() + { + _installer = new InstallerService(); + _embeddedDataReader = new EmbeddedServerDataReader(); - CopyCommandLineArgs(); + CopyCommandLineArgs(); - ExtractEmbeddedServerData().Wait(); + ExtractEmbeddedServerData().Wait(); - AddExistingConnectionInfo(); - } + AddExistingConnectionInfo(); + } - public bool CreateSupportShortcut + public bool CreateSupportShortcut + { + get { - get - { - return _createSupportShortcut; - } - set - { - _createSupportShortcut = value; - FirePropertyChanged(); - } + return _createSupportShortcut; } + set + { + _createSupportShortcut = value; + FirePropertyChanged(); + } + } - public string HeaderMessage + public string HeaderMessage + { + get { - get - { - return _headerMessage; - } - set - { - _headerMessage = value; - FirePropertyChanged(); - } + return _headerMessage; + } + set + { + _headerMessage = value; + FirePropertyChanged(); } + } - public BitmapImage Icon { get; set; } - public string InstallButtonText => IsServiceMissing ? "Install" : "Reinstall"; + public BitmapImage? Icon { get; set; } + public string InstallButtonText => IsServiceMissing ? "Install" : "Reinstall"; - public ICommand InstallCommand => new RelayCommand(async (param) => { await Install(); }); + public ICommand InstallCommand => new RelayCommand(async (param) => { await Install(); }); - public bool IsProgressVisible => Progress > 0; + public bool IsProgressVisible => Progress > 0; - public bool IsReadyState + public bool IsReadyState + { + get { - get - { - return _isReadyState; - } - set - { - _isReadyState = value; - FirePropertyChanged(); - } + return _isReadyState; + } + set + { + _isReadyState = value; + FirePropertyChanged(); } + } - public bool IsServiceInstalled + public bool IsServiceInstalled + { + get { - get - { - return _isServiceInstalled; - } - set - { - _isServiceInstalled = value; - FirePropertyChanged(); - FirePropertyChanged(nameof(IsServiceMissing)); - FirePropertyChanged(nameof(InstallButtonText)); - } + return _isServiceInstalled; + } + set + { + _isServiceInstalled = value; + FirePropertyChanged(); + FirePropertyChanged(nameof(IsServiceMissing)); + FirePropertyChanged(nameof(InstallButtonText)); } + } - public bool IsServiceMissing => !_isServiceInstalled; + public bool IsServiceMissing => !_isServiceInstalled; - public ICommand OpenLogsCommand + public ICommand OpenLogsCommand + { + get { - get + return new RelayCommand(param => { - return new RelayCommand(param => + + if (Directory.Exists(Logger.LogsDir)) { - - if (Directory.Exists(Logger.LogsDir)) - { - Process.Start(Logger.LogsDir); - } - else - { - MessageBoxEx.Show("Log directory doesn't exist.", "No Logs", MessageBoxButton.OK, MessageBoxImage.Information); - } - }); - } + Process.Start(Logger.LogsDir); + } + else + { + MessageBoxEx.Show("Log directory doesn't exist.", "No Logs", MessageBoxButton.OK, MessageBoxImage.Information); + } + }); } + } - public string OrganizationID + public string? OrganizationID + { + get { - get - { - return _organizationID; - } - set - { - _organizationID = value; - FirePropertyChanged(); - } + return _organizationID; + } + set + { + _organizationID = value; + FirePropertyChanged(); } + } - public string ProductName { get; set; } + public string ProductName { get; set; } = "Remotely"; - public int Progress + public int Progress + { + get { - get - { - return _progress; - } - set - { - _progress = value; - FirePropertyChanged(); - FirePropertyChanged(nameof(IsProgressVisible)); - } + return _progress; } + set + { + _progress = value; + FirePropertyChanged(); + FirePropertyChanged(nameof(IsProgressVisible)); + } + } - public string ServerUrl + public string ServerUrl + { + get { - get - { - return _serverUrl; - } - set - { - _serverUrl = value?.TrimEnd('/'); - FirePropertyChanged(); - } + return _serverUrl; } + set + { + _serverUrl = value?.TrimEnd('/') ?? string.Empty; + FirePropertyChanged(); + } + } - public string StatusMessage + public string? StatusMessage + { + get { - get - { - return _statusMessage; - } - set - { - _statusMessage = value; - FirePropertyChanged(); - } + return _statusMessage; + } + set + { + _statusMessage = value; + FirePropertyChanged(); } + } - public SolidColorBrush TitleBackgroundColor { get; set; } - public SolidColorBrush TitleButtonForegroundColor { get; set; } - public SolidColorBrush TitleForegroundColor { get; set; } - public ICommand UninstallCommand => new RelayCommand(async (param) => { await Uninstall(); }); - private string DeviceAlias { get; set; } - private string DeviceGroup { get; set; } - private string DeviceUuid { get; set; } - public async Task Init() + public SolidColorBrush? TitleBackgroundColor { get; set; } + public SolidColorBrush? TitleButtonForegroundColor { get; set; } + public SolidColorBrush? TitleForegroundColor { get; set; } + public ICommand UninstallCommand => new RelayCommand(async (param) => { await Uninstall(); }); + private string? DeviceAlias { get; set; } + private string? DeviceGroup { get; set; } + private string? DeviceUuid { get; set; } + + public async Task Init() + { + _installer.ProgressMessageChanged += (sender, arg) => { - _installer.ProgressMessageChanged += (sender, arg) => - { - StatusMessage = arg; - }; + StatusMessage = arg; + }; - _installer.ProgressValueChanged += (sender, arg) => - { - Progress = arg; - }; + _installer.ProgressValueChanged += (sender, arg) => + { + Progress = arg; + }; - IsServiceInstalled = ServiceController.GetServices().Any(x => x.ServiceName == "Remotely_Service"); - if (IsServiceMissing) - { - HeaderMessage = $"Install the {ProductName} service."; - } - else - { - HeaderMessage = $"Modify the {ProductName} installation."; - } + IsServiceInstalled = ServiceController.GetServices().Any(x => x.ServiceName == "Remotely_Service"); + if (IsServiceMissing) + { + HeaderMessage = $"Install the {ProductName} service."; + } + else + { + HeaderMessage = $"Modify the {ProductName} installation."; + } - CommandLineParser.VerifyArguments(); + CommandLineParser.VerifyArguments(); - if (CommandLineParser.CommandLineArgs.ContainsKey("install")) - { - await Install(); - } - else if (CommandLineParser.CommandLineArgs.ContainsKey("uninstall")) - { - await Uninstall(); - } + if (CommandLineParser.CommandLineArgs.ContainsKey("install")) + { + await Install(); + } + else if (CommandLineParser.CommandLineArgs.ContainsKey("uninstall")) + { + await Uninstall(); + } - if (CommandLineParser.CommandLineArgs.ContainsKey("quiet")) - { - App.Current.Shutdown(); - } + if (CommandLineParser.CommandLineArgs.ContainsKey("quiet")) + { + App.Current.Shutdown(); } + } - private void AddExistingConnectionInfo() + private void AddExistingConnectionInfo() + { + try { - try + var connectionInfoPath = Path.Combine( + Path.GetPathRoot(Environment.SystemDirectory), + "Program Files", + "Remotely", + "ConnectionInfo.json"); + + if (File.Exists(connectionInfoPath)) { - var connectionInfoPath = Path.Combine( - Path.GetPathRoot(Environment.SystemDirectory), - "Program Files", - "Remotely", - "ConnectionInfo.json"); + var serializer = new JavaScriptSerializer(); + var connectionInfo = serializer.Deserialize(File.ReadAllText(connectionInfoPath)); - if (File.Exists(connectionInfoPath)) + if (string.IsNullOrWhiteSpace(OrganizationID)) { - var serializer = new JavaScriptSerializer(); - var connectionInfo = serializer.Deserialize(File.ReadAllText(connectionInfoPath)); - - if (string.IsNullOrWhiteSpace(OrganizationID)) - { - OrganizationID = connectionInfo.OrganizationID; - } - - if (string.IsNullOrWhiteSpace(ServerUrl)) - { - ServerUrl = connectionInfo.Host; - } + OrganizationID = connectionInfo.OrganizationID; } - } - catch (Exception ex) - { - Logger.Write(ex); - } + if (string.IsNullOrWhiteSpace(ServerUrl)) + { + ServerUrl = connectionInfo.Host ?? string.Empty; + } + } + } + catch (Exception ex) + { + Logger.Write(ex); } - private void ApplyBranding(BrandingInfo brandingInfo) + } + + private void ApplyBranding(BrandingInfo? brandingInfo) + { + try { - try + if (brandingInfo is not null && + !string.IsNullOrWhiteSpace(brandingInfo.Product)) { - ProductName = "Remotely"; + ProductName = brandingInfo.Product; + } - if (!string.IsNullOrWhiteSpace(brandingInfo?.Product)) - { - ProductName = brandingInfo.Product; - } + TitleBackgroundColor = new SolidColorBrush(Color.FromRgb( + brandingInfo?.TitleBackgroundRed ?? 70, + brandingInfo?.TitleBackgroundGreen ?? 70, + brandingInfo?.TitleBackgroundBlue ?? 70)); - TitleBackgroundColor = new SolidColorBrush(Color.FromRgb( - brandingInfo?.TitleBackgroundRed ?? 70, - brandingInfo?.TitleBackgroundGreen ?? 70, - brandingInfo?.TitleBackgroundBlue ?? 70)); + TitleForegroundColor = new SolidColorBrush(Color.FromRgb( + brandingInfo?.TitleForegroundRed ?? 29, + brandingInfo?.TitleForegroundGreen ?? 144, + brandingInfo?.TitleForegroundBlue ?? 241)); - TitleForegroundColor = new SolidColorBrush(Color.FromRgb( - brandingInfo?.TitleForegroundRed ?? 29, - brandingInfo?.TitleForegroundGreen ?? 144, - brandingInfo?.TitleForegroundBlue ?? 241)); + TitleButtonForegroundColor = new SolidColorBrush(Color.FromRgb( + brandingInfo?.ButtonForegroundRed ?? 255, + brandingInfo?.ButtonForegroundGreen ?? 255, + brandingInfo?.ButtonForegroundBlue ?? 255)); - TitleButtonForegroundColor = new SolidColorBrush(Color.FromRgb( - brandingInfo?.ButtonForegroundRed ?? 255, - brandingInfo?.ButtonForegroundGreen ?? 255, - brandingInfo?.ButtonForegroundBlue ?? 255)); + TitleBackgroundColor.Freeze(); + TitleForegroundColor.Freeze(); + TitleButtonForegroundColor.Freeze(); - TitleBackgroundColor.Freeze(); - TitleForegroundColor.Freeze(); - TitleButtonForegroundColor.Freeze(); + Icon = GetBitmapImageIcon(brandingInfo); + } + catch (Exception ex) + { + Logger.Write(ex); + } + } - Icon = GetBitmapImageIcon(brandingInfo); - } - catch (Exception ex) - { - Logger.Write(ex); - } + private bool CheckIsAdministrator() + { + var identity = WindowsIdentity.GetCurrent(); + var principal = new WindowsPrincipal(identity); + var result = principal.IsInRole(WindowsBuiltInRole.Administrator); + if (!result) + { + MessageBoxEx.Show("Elevated privileges are required. Please restart the installer using 'Run as administrator'.", "Elevation Required", MessageBoxButton.OK, MessageBoxImage.Warning); } + return result; + } - private bool CheckIsAdministrator() + private bool CheckParams() + { + if (string.IsNullOrWhiteSpace(OrganizationID) || string.IsNullOrWhiteSpace(ServerUrl)) { - var identity = WindowsIdentity.GetCurrent(); - var principal = new WindowsPrincipal(identity); - var result = principal.IsInRole(WindowsBuiltInRole.Administrator); - if (!result) - { - MessageBoxEx.Show("Elevated privileges are required. Please restart the installer using 'Run as administrator'.", "Elevation Required", MessageBoxButton.OK, MessageBoxImage.Warning); - } - return result; + Logger.Write("ServerUrl or OrganizationID param is missing. Unable to install."); + MessageBoxEx.Show("Required settings are missing. Please enter a server URL and organization ID.", "Invalid Input", MessageBoxButton.OK, MessageBoxImage.Error); + return false; } - private bool CheckParams() + if (!Guid.TryParse(OrganizationID, out _)) { - if (string.IsNullOrWhiteSpace(OrganizationID) || string.IsNullOrWhiteSpace(ServerUrl)) - { - Logger.Write("ServerUrl or OrganizationID param is missing. Unable to install."); - MessageBoxEx.Show("Required settings are missing. Please enter a server URL and organization ID.", "Invalid Input", MessageBoxButton.OK, MessageBoxImage.Error); - return false; - } + Logger.Write("OrganizationID is not a valid GUID."); + MessageBoxEx.Show("Organization ID must be a valid GUID.", "Invalid Organization ID", MessageBoxButton.OK, MessageBoxImage.Error); + return false; + } - if (!Guid.TryParse(OrganizationID, out _)) - { - Logger.Write("OrganizationID is not a valid GUID."); - MessageBoxEx.Show("Organization ID must be a valid GUID.", "Invalid Organization ID", MessageBoxButton.OK, MessageBoxImage.Error); - return false; - } + if (!Uri.TryCreate(ServerUrl, UriKind.Absolute, out var serverUri) || + (serverUri.Scheme != Uri.UriSchemeHttp && serverUri.Scheme != Uri.UriSchemeHttps)) + { + Logger.Write("ServerUrl is not valid."); + MessageBoxEx.Show("Server URL must be a valid Uri (e.g. https://app.remotely.one).", "Invalid Server URL", MessageBoxButton.OK, MessageBoxImage.Error); + return false; + } - if (!Uri.TryCreate(ServerUrl, UriKind.Absolute, out var serverUri) || - (serverUri.Scheme != Uri.UriSchemeHttp && serverUri.Scheme != Uri.UriSchemeHttps)) - { - Logger.Write("ServerUrl is not valid."); - MessageBoxEx.Show("Server URL must be a valid Uri (e.g. https://app.remotely.one).", "Invalid Server URL", MessageBoxButton.OK, MessageBoxImage.Error); - return false; - } + return true; + } - return true; + private void CopyCommandLineArgs() + { + if (CommandLineParser.CommandLineArgs.TryGetValue("organizationid", out var orgID)) + { + OrganizationID = orgID; } - private void CopyCommandLineArgs() + if (CommandLineParser.CommandLineArgs.TryGetValue("serverurl", out var serverUrl)) { - if (CommandLineParser.CommandLineArgs.TryGetValue("organizationid", out var orgID)) - { - OrganizationID = orgID; - } + ServerUrl = serverUrl; + } - if (CommandLineParser.CommandLineArgs.TryGetValue("serverurl", out var serverUrl)) - { - ServerUrl = serverUrl; - } + if (CommandLineParser.CommandLineArgs.TryGetValue("devicegroup", out var deviceGroup)) + { + DeviceGroup = deviceGroup; + } - if (CommandLineParser.CommandLineArgs.TryGetValue("devicegroup", out var deviceGroup)) - { - DeviceGroup = deviceGroup; - } + if (CommandLineParser.CommandLineArgs.TryGetValue("devicealias", out var deviceAlias)) + { + DeviceAlias = deviceAlias; + } + + if (CommandLineParser.CommandLineArgs.TryGetValue("deviceuuid", out var deviceUuid)) + { + DeviceUuid = deviceUuid; + } - if (CommandLineParser.CommandLineArgs.TryGetValue("devicealias", out var deviceAlias)) + if (CommandLineParser.CommandLineArgs.ContainsKey("supportshortcut")) + { + CreateSupportShortcut = true; + } + } + + private async Task ExtractEmbeddedServerData() + { + + try + { + var filePath = Process.GetCurrentProcess()?.MainModule?.FileName; + + if (string.IsNullOrWhiteSpace(filePath)) { - DeviceAlias = deviceAlias; + Logger.Write("Failed to retrieve executing file name."); + return; } - if (CommandLineParser.CommandLineArgs.TryGetValue("deviceuuid", out var deviceUuid)) + var embeddedData = await _embeddedDataReader.TryGetEmbeddedData(filePath); + + if (embeddedData is null || embeddedData == EmbeddedServerData.Empty) { - DeviceUuid = deviceUuid; + Logger.Write("Embedded server data is empty. Aborting."); + return; } - if (CommandLineParser.CommandLineArgs.ContainsKey("supportshortcut")) + if (embeddedData.ServerUrl is null) { - CreateSupportShortcut = true; + Logger.Write("ServerUrl is empty. Aborting."); + return; } - } - private async Task ExtractEmbeddedServerData() - { + OrganizationID = embeddedData.OrganizationId; + ServerUrl = embeddedData.ServerUrl.AbsoluteUri; - try + using (var httpClient = new HttpClient()) { - var filePath = Process.GetCurrentProcess()?.MainModule?.FileName; - - if (string.IsNullOrWhiteSpace(filePath)) + var serializer = new JavaScriptSerializer(); + var brandingUrl = $"{ServerUrl.TrimEnd('/')}/api/branding/{OrganizationID}"; + using (var response = await httpClient.GetAsync(brandingUrl).ConfigureAwait(false)) { - Logger.Write("Failed to retrieve executing file name."); - return; - } - - var embeddedData = await _embeddedDataReader.TryGetEmbeddedData(filePath); + var responseString = await response.Content.ReadAsStringAsync(); + _brandingInfo = serializer.Deserialize(responseString); - if (embeddedData is null || embeddedData == EmbeddedServerData.Empty) - { - Logger.Write("Embedded server data is empty. Aborting."); - return; } - - if (embeddedData.ServerUrl is null) - { - Logger.Write("ServerUrl is empty. Aborting."); - return; - } - - OrganizationID = embeddedData.OrganizationId; - ServerUrl = embeddedData.ServerUrl.AbsoluteUri; - - using (var httpClient = new HttpClient()) - { - var serializer = new JavaScriptSerializer(); - var brandingUrl = $"{ServerUrl.TrimEnd('/')}/api/branding/{OrganizationID}"; - using (var response = await httpClient.GetAsync(brandingUrl).ConfigureAwait(false)) - { - var responseString = await response.Content.ReadAsStringAsync(); - _brandingInfo = serializer.Deserialize(responseString); - - } - } - } - catch (Exception ex) - { - Logger.Write(ex); - } - finally - { - ApplyBranding(_brandingInfo); } } - private BitmapImage GetBitmapImageIcon(BrandingInfo bi) + catch (Exception ex) { - Stream imageStream; - if (!string.IsNullOrWhiteSpace(bi?.Icon)) - { - imageStream = new MemoryStream(Convert.FromBase64String(bi.Icon)); - } - else - { - imageStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Remotely.Agent.Installer.Win.Assets.Remotely_Icon.png"); - } + Logger.Write(ex); + } + finally + { + ApplyBranding(_brandingInfo); + } + } + private BitmapImage GetBitmapImageIcon(BrandingInfo? bi) + { + Stream imageStream; + if (bi is not null && + !string.IsNullOrWhiteSpace(bi.Icon)) + { + imageStream = new MemoryStream(Convert.FromBase64String(bi.Icon)); + } + else + { + imageStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Remotely.Agent.Installer.Win.Assets.Remotely_Icon.png"); + } - var bitmap = new BitmapImage(); - bitmap.BeginInit(); - bitmap.StreamSource = imageStream; - bitmap.CacheOption = BitmapCacheOption.OnLoad; - bitmap.EndInit(); - bitmap.Freeze(); - imageStream.Close(); + var bitmap = new BitmapImage(); + bitmap.BeginInit(); + bitmap.StreamSource = imageStream; + bitmap.CacheOption = BitmapCacheOption.OnLoad; + bitmap.EndInit(); + bitmap.Freeze(); + imageStream.Close(); - return bitmap; - } - private async Task Install() + return bitmap; + } + private async Task Install() + { + try { - try + IsReadyState = false; + if (!CheckParams()) { - IsReadyState = false; - if (!CheckParams()) - { - return; - } + return; + } - HeaderMessage = "Installing Remotely..."; + HeaderMessage = "Installing Remotely..."; - if (await _installer.Install(ServerUrl, OrganizationID, DeviceGroup, DeviceAlias, DeviceUuid, CreateSupportShortcut)) - { - IsServiceInstalled = true; - Progress = 0; - HeaderMessage = "Installation completed."; - StatusMessage = "Remotely has been installed. You can now close this window."; - } - else - { - Progress = 0; - HeaderMessage = "An error occurred during installation."; - StatusMessage = "There was an error during installation. Check the logs for details."; - } - if (!CheckIsAdministrator()) - { - return; - } + if (await _installer.Install(ServerUrl, OrganizationID, DeviceGroup, DeviceAlias, DeviceUuid, CreateSupportShortcut)) + { + IsServiceInstalled = true; + Progress = 0; + HeaderMessage = "Installation completed."; + StatusMessage = "Remotely has been installed. You can now close this window."; } - catch (Exception ex) + else { - Logger.Write(ex); + Progress = 0; + HeaderMessage = "An error occurred during installation."; + StatusMessage = "There was an error during installation. Check the logs for details."; } - finally + if (!CheckIsAdministrator()) { - IsReadyState = true; + return; } } - - private async Task Uninstall() + catch (Exception ex) { - try - { - IsReadyState = false; + Logger.Write(ex); + } + finally + { + IsReadyState = true; + } + } - HeaderMessage = "Uninstalling Remotely..."; + private async Task Uninstall() + { + try + { + IsReadyState = false; - if (await _installer.Uninstall()) - { - IsServiceInstalled = false; - Progress = 0; - HeaderMessage = "Uninstall completed."; - StatusMessage = "Remotely has been uninstalled. You can now close this window."; - } - else - { - Progress = 0; - HeaderMessage = "An error occurred during uninstall."; - StatusMessage = "There was an error during uninstall. Check the logs for details."; - } + HeaderMessage = "Uninstalling Remotely..."; - } - catch (Exception ex) + if (await _installer.Uninstall()) { - Logger.Write(ex); + IsServiceInstalled = false; + Progress = 0; + HeaderMessage = "Uninstall completed."; + StatusMessage = "Remotely has been uninstalled. You can now close this window."; } - finally + else { - IsReadyState = true; + Progress = 0; + HeaderMessage = "An error occurred during uninstall."; + StatusMessage = "There was an error during uninstall. Check the logs for details."; } + + } + catch (Exception ex) + { + Logger.Write(ex); + } + finally + { + IsReadyState = true; } } } diff --git a/Agent.Installer.Win/ViewModels/ViewModelBase.cs b/Agent.Installer.Win/ViewModels/ViewModelBase.cs index a864d5991..15fecde76 100644 --- a/Agent.Installer.Win/ViewModels/ViewModelBase.cs +++ b/Agent.Installer.Win/ViewModels/ViewModelBase.cs @@ -1,15 +1,16 @@ -using System.ComponentModel; +#nullable enable + +using System.ComponentModel; using System.Runtime.CompilerServices; -namespace Remotely.Agent.Installer.Win.ViewModels +namespace Remotely.Agent.Installer.Win.ViewModels; + +public class ViewModelBase : INotifyPropertyChanged { - public class ViewModelBase : INotifyPropertyChanged - { - public event PropertyChangedEventHandler PropertyChanged; + public event PropertyChangedEventHandler? PropertyChanged; - public void FirePropertyChanged([CallerMemberName]string propertyName = "") - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } + public void FirePropertyChanged([CallerMemberName]string propertyName = "") + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } diff --git a/Agent/Agent.csproj b/Agent/Agent.csproj index f02f3708a..30150989e 100644 --- a/Agent/Agent.csproj +++ b/Agent/Agent.csproj @@ -16,6 +16,7 @@ Remotely_Agent Remotely.Agent Assets\favicon.ico + enable diff --git a/Agent/Program.cs b/Agent/Program.cs index 66ffd7dbd..7afc93ee5 100644 --- a/Agent/Program.cs +++ b/Agent/Program.cs @@ -23,7 +23,7 @@ namespace Remotely.Agent; public class Program { [Obsolete("Remove this when all services are in DI behind interfaces.")] - public static IServiceProvider Services { get; set; } + public static IServiceProvider? Services { get; set; } public static async Task Main(string[] args) { @@ -151,7 +151,7 @@ private static void SetSas(IServiceProvider services, ILogger logger) private static void SetWorkingDirectory() { var exePath = Environment.ProcessPath ?? Environment.GetCommandLineArgs().First(); - var exeDir = Path.GetDirectoryName(exePath); + var exeDir = Path.GetDirectoryName(exePath) ?? AppDomain.CurrentDomain.BaseDirectory; Directory.SetCurrentDirectory(exeDir); } } diff --git a/Desktop.Linux/Desktop.Linux.csproj b/Desktop.Linux/Desktop.Linux.csproj index 18e52e65e..ae1112083 100644 --- a/Desktop.Linux/Desktop.Linux.csproj +++ b/Desktop.Linux/Desktop.Linux.csproj @@ -31,6 +31,7 @@ Desktop client for allowing your IT admin to provide remote support. Copyright © 2023 Immense Networks https://remotely.one + enable diff --git a/Desktop.Win/Desktop.Win.csproj b/Desktop.Win/Desktop.Win.csproj index 752ab7a78..3d4049825 100644 --- a/Desktop.Win/Desktop.Win.csproj +++ b/Desktop.Win/Desktop.Win.csproj @@ -18,6 +18,7 @@ AnyCPU;x86;x64 True + enable diff --git a/Server/API/AlertsController.cs b/Server/API/AlertsController.cs index 1c850cf1b..56bfa61c5 100644 --- a/Server/API/AlertsController.cs +++ b/Server/API/AlertsController.cs @@ -8,6 +8,7 @@ using System.IO; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; using System.Threading.Tasks; @@ -39,7 +40,7 @@ public AlertsController( [HttpPost("Create")] public async Task Create(AlertOptions alertOptions) { - Request.Headers.TryGetValue("OrganizationID", out var orgID); + _ = Request.Headers.TryGetValue("OrganizationID", out var orgId); _logger.LogInformation("Alert created. Alert Options: {options}", JsonSerializer.Serialize(alertOptions)); @@ -47,7 +48,7 @@ public async Task Create(AlertOptions alertOptions) { try { - await _dataService.AddAlert(alertOptions.AlertDeviceID, orgID, alertOptions.AlertMessage); + await _dataService.AddAlert(alertOptions.AlertDeviceID, orgId.ToString(), alertOptions.AlertMessage); } catch (Exception ex) { @@ -62,7 +63,7 @@ public async Task Create(AlertOptions alertOptions) await _emailSender.SendEmailAsync(alertOptions.EmailTo, alertOptions.EmailSubject, alertOptions.EmailBody, - orgID); + orgId.ToString()); } catch (Exception ex) { @@ -81,7 +82,7 @@ await _emailSender.SendEmailAsync(alertOptions.EmailTo, alertOptions.ApiRequestUrl); request.Content = new StringContent(alertOptions.ApiRequestBody); - request.Content.Headers.ContentType.MediaType = "application/json"; + request.Content.Headers.ContentType = new("application/json"); foreach (var header in alertOptions.ApiRequestHeaders) { @@ -121,15 +122,15 @@ public async Task Delete(string alertID) [HttpDelete("DeleteAll")] public async Task DeleteAll() { - Request.Headers.TryGetValue("OrganizationID", out var orgID); + Request.Headers.TryGetValue("OrganizationID", out var orgId); - if (User.Identity.IsAuthenticated) + if (User.Identity?.IsAuthenticated == true) { - await _dataService.DeleteAllAlerts(orgID, User.Identity.Name); + await _dataService.DeleteAllAlerts(orgId.ToString(), User?.Identity?.Name); } else { - await _dataService.DeleteAllAlerts(orgID); + await _dataService.DeleteAllAlerts(orgId.ToString()); } return Ok(); diff --git a/Server/API/OrganizationManagementController.cs b/Server/API/OrganizationManagementController.cs index 6a6ddc8ae..1487a6438 100644 --- a/Server/API/OrganizationManagementController.cs +++ b/Server/API/OrganizationManagementController.cs @@ -122,7 +122,7 @@ public IActionResult DeviceGroup([FromBody] string deviceGroupID) [HttpPost("DeviceGroup")] [ServiceFilter(typeof(ApiAuthorizationFilter))] - public IActionResult DeviceGroup([FromBody] DeviceGroup deviceGroup) + public async Task DeviceGroup([FromBody] DeviceGroup deviceGroup) { if (User.Identity.IsAuthenticated && !DataService.GetUserByNameWithOrg(User.Identity.Name).IsAdministrator) @@ -135,13 +135,14 @@ public IActionResult DeviceGroup([FromBody] DeviceGroup deviceGroup) return BadRequest(); } - Request.Headers.TryGetValue("OrganizationID", out var orgID); - var result = DataService.AddDeviceGroup(orgID, deviceGroup, out var deviceGroupID, out var errorMessage); - if (!result) + Request.Headers.TryGetValue("OrganizationID", out var orgId); + + var result = await DataService.AddDeviceGroup($"{orgId}", deviceGroup); + if (!result.IsSuccess) { - return BadRequest(errorMessage); + return BadRequest(result.Reason); } - return Ok(deviceGroupID); + return Ok(result.Value.ID); } [HttpDelete("DeviceGroup/{groupID}/Users/")] diff --git a/Server/Areas/Identity/Pages/Account/Register.cshtml.cs b/Server/Areas/Identity/Pages/Account/Register.cshtml.cs index dee393af7..ed8de7f64 100644 --- a/Server/Areas/Identity/Pages/Account/Register.cshtml.cs +++ b/Server/Areas/Identity/Pages/Account/Register.cshtml.cs @@ -94,7 +94,7 @@ public async Task OnPostAsync(string returnUrl = null) UserName = Input.Email, Email = Input.Email, IsServerAdmin = organizationCount == 0, - Organization = new Organization(), + Organization = new Organization() { OrganizationName = "Test Org" }, UserOptions = new RemotelyUserOptions(), IsAdministrator = true, LockoutEnabled = true diff --git a/Server/Components/Scripts/SavedScripts.razor.cs b/Server/Components/Scripts/SavedScripts.razor.cs index 20a4af3e8..17b5754b4 100644 --- a/Server/Components/Scripts/SavedScripts.razor.cs +++ b/Server/Components/Scripts/SavedScripts.razor.cs @@ -19,7 +19,7 @@ public partial class SavedScripts : AuthComponentBase [CascadingParameter] private ScriptsPage ParentPage { get; set; } - private SavedScript _selectedScript = new(); + private SavedScript _selectedScript = new() { Name = "Test Script" }; private string _alertMessage; private string _alertOptionsShowClass; private string _environmentVarsShowClass; @@ -72,7 +72,10 @@ private async Task OnValidSubmit(EditContext context) private void CreateNew() { - _selectedScript = new(); + _selectedScript = new() + { + Name = "Test Script" + }; } private async Task DeleteSelectedScript() @@ -90,7 +93,10 @@ private async Task DeleteSelectedScript() ToastService.ShowToast("Script deleted."); _alertMessage = "Script deleted."; await ParentPage.RefreshScripts(); - _selectedScript = new(); + _selectedScript = new() + { + Name = "Test Script" + }; } } @@ -98,11 +104,17 @@ private async Task ScriptSelected(ScriptTreeNode viewModel) { if (viewModel.Script is not null) { - _selectedScript = await DataService.GetSavedScript(User.Id, viewModel.Script.Id) ?? new(); + _selectedScript = await DataService.GetSavedScript(User.Id, viewModel.Script.Id) ?? new() + { + Name = "Test Script" + }; } else { - _selectedScript = new(); + _selectedScript = new() + { + Name = "Test Script" + }; } } diff --git a/Server/Components/Scripts/ScriptSchedules.razor.cs b/Server/Components/Scripts/ScriptSchedules.razor.cs index 8a2f9e7cf..063d7399b 100644 --- a/Server/Components/Scripts/ScriptSchedules.razor.cs +++ b/Server/Components/Scripts/ScriptSchedules.razor.cs @@ -65,7 +65,10 @@ protected override async Task OnInitializedAsync() private void CreateNew() { - _selectedScript = new(); + _selectedScript = new() + { + Name = "Test Script" + }; _selectedSchedule = new() { StartAt = Time.Now }; _selectedDeviceGroups.Clear(); _selectedDevices.Clear(); diff --git a/Server/Pages/ApiKeys.razor b/Server/Pages/ApiKeys.razor index 7b5dd16f0..ddb40a5cf 100644 --- a/Server/Pages/ApiKeys.razor +++ b/Server/Pages/ApiKeys.razor @@ -1,4 +1,5 @@ @page "/api-keys" +@using Immense.RemoteControl.Shared.Helpers; @attribute [Authorize(Policy = PolicyNames.OrganizationAdminRequired)] @inherits AuthComponentBase @@ -6,6 +7,7 @@ @inject AuthenticationStateProvider AuthProvider @inject IJsInterop JsInterop @inject IModalService ModalService +@inject IToastService ToastService

API Keys

@@ -76,9 +78,9 @@ else @code { private readonly List _apiTokens = new(); - private string _alertMessage; - private string _createKeyName; - private string _newKeySecret; + private string _alertMessage = string.Empty; + private string _createKeyName = string.Empty; + private string _newKeySecret = string.Empty; protected override async Task OnInitializedAsync() { @@ -88,8 +90,8 @@ else private async Task CreateNewKey() { - var secret = PasswordGenerator.GeneratePassword(36); - var secretHash = new PasswordHasher().HashPassword(null, secret); + var secret = RandomGenerator.GenerateString(36); + var secretHash = new PasswordHasher().HashPassword(string.Empty, secret); await DataService.CreateApiToken(Username, _createKeyName, secretHash); RefreshData(); @@ -100,12 +102,19 @@ else private async Task DeleteKey(string keyId) { var result = await JsInterop.Confirm("Are you sure you want to delete this key?"); - if (result) + if (!result) { - await DataService.DeleteApiToken(Username, keyId); - RefreshData(); - _alertMessage = "Key deleted."; + return; } + + var deleteResult = await DataService.DeleteApiToken(Username, keyId); + if (!deleteResult.IsSuccess) + { + ToastService.ShowToast2(deleteResult.Reason, ToastType.Error); + return; + } + RefreshData(); + _alertMessage = "Key deleted."; } private void RefreshData() diff --git a/Server/Pages/ManageOrganization.razor.cs b/Server/Pages/ManageOrganization.razor.cs index 484e0c075..b783dc115 100644 --- a/Server/Pages/ManageOrganization.razor.cs +++ b/Server/Pages/ManageOrganization.razor.cs @@ -57,7 +57,7 @@ protected override async Task OnInitializedAsync() await RefreshData(); } - private void CreateNewDeviceGroup() + private async Task CreateNewDeviceGroup() { if (!User.IsAdministrator) { @@ -74,10 +74,10 @@ private void CreateNewDeviceGroup() Name = _newDeviceGroupName }; - var result = DataService.AddDeviceGroup(User.OrganizationID, deviceGroup, out _, out var errorMessage); - if (!result) + var result = await DataService.AddDeviceGroup(User.OrganizationID, deviceGroup); + if (!result.IsSuccess) { - ToastService.ShowToast(errorMessage, classString: "bg-danger"); + ToastService.ShowToast(result.Reason, classString: "bg-danger"); return; } @@ -185,11 +185,11 @@ private async Task EvaluateInviteInputKeypress(KeyboardEventArgs args) } } - private void EvaluateNewDeviceGroupKeyPress(KeyboardEventArgs args) + private async Task EvaluateNewDeviceGroupKeyPress(KeyboardEventArgs args) { if (args.Key.Equals("Enter", StringComparison.OrdinalIgnoreCase)) { - CreateNewDeviceGroup(); + await CreateNewDeviceGroup(); } } private void OrganizationNameChanged(ChangeEventArgs args) @@ -199,7 +199,11 @@ private void OrganizationNameChanged(ChangeEventArgs args) return; } - var newName = (string)args.Value; + if (args.Value is not string newName) + { + return; + } + if (string.IsNullOrWhiteSpace(newName)) { return; diff --git a/Server/Server.csproj b/Server/Server.csproj index 2b84d4e06..ec05af72e 100644 --- a/Server/Server.csproj +++ b/Server/Server.csproj @@ -11,6 +11,7 @@ false + enable diff --git a/Server/Services/DataService.cs b/Server/Services/DataService.cs index 4025621a5..acec9be48 100644 --- a/Server/Services/DataService.cs +++ b/Server/Services/DataService.cs @@ -25,9 +25,9 @@ namespace Remotely.Server.Services; // TODO: Separate this into domain-specific services. public interface IDataService { - Task AddAlert(string deviceID, string organizationID, string alertMessage, string details = null); + Task AddAlert(string deviceID, string organizationID, string alertMessage, string? details = null); - bool AddDeviceGroup(string orgID, DeviceGroup deviceGroup, out string deviceGroupID, out string errorMessage); + Task> AddDeviceGroup(string orgID, DeviceGroup deviceGroup); Task AddDeviceToGroup(string deviceId, string groupId); InviteLink AddInvite(string orgID, InviteViewModel invite); @@ -61,9 +61,9 @@ public interface IDataService Task DeleteAlert(Alert alert); - Task DeleteAllAlerts(string orgID, string userName = null); + Task DeleteAllAlerts(string orgID, string? userName = null); - Task DeleteApiToken(string userName, string tokenId); + Task DeleteApiToken(string userName, string tokenId); void DeleteDeviceGroup(string orgID, string deviceGroupID); @@ -243,7 +243,7 @@ public DataService( _logger = logger; } - public async Task AddAlert(string deviceId, string organizationID, string alertMessage, string details = null) + public async Task AddAlert(string deviceId, string organizationID, string alertMessage, string? details = null) { using var dbContext = _appDbFactory.GetContext(); @@ -277,23 +277,24 @@ await users.ForEachAsync(x => await dbContext.SaveChangesAsync(); } - public bool AddDeviceGroup(string orgID, DeviceGroup deviceGroup, out string deviceGroupID, out string errorMessage) + public async Task> AddDeviceGroup(string orgID, DeviceGroup deviceGroup) { using var dbContext = _appDbFactory.GetContext(); - deviceGroupID = null; - errorMessage = null; - var organization = dbContext.Organizations .Include(x => x.DeviceGroups) .FirstOrDefault(x => x.ID == orgID); + if (organization is null) + { + return Result.Fail("Organization not found."); + } + if (dbContext.DeviceGroups.Any(x => x.OrganizationID == orgID && x.Name.ToLower() == deviceGroup.Name.ToLower())) { - errorMessage = "Device group already exists."; - return false; + return Result.Fail("Device group already exists."); } dbContext.Attach(deviceGroup); @@ -301,9 +302,8 @@ public bool AddDeviceGroup(string orgID, DeviceGroup deviceGroup, out string dev deviceGroup.OrganizationID = orgID; organization.DeviceGroups.Add(deviceGroup); - dbContext.SaveChanges(); - deviceGroupID = deviceGroup.ID; - return true; + await dbContext.SaveChangesAsync(); + return Result.Ok(deviceGroup); } public async Task AddDeviceToGroup(string deviceId, string groupId) @@ -762,7 +762,7 @@ public async Task DeleteAlert(Alert alert) await dbContext.SaveChangesAsync(); } - public async Task DeleteAllAlerts(string orgID, string userName = null) + public async Task DeleteAllAlerts(string orgID, string? userName = null) { using var dbContext = _appDbFactory.GetContext(); @@ -779,17 +779,29 @@ public async Task DeleteAllAlerts(string orgID, string userName = null) await dbContext.SaveChangesAsync(); } - public async Task DeleteApiToken(string userName, string tokenId) + public async Task DeleteApiToken(string userName, string tokenId) { using var dbContext = _appDbFactory.GetContext(); var user = dbContext.Users.FirstOrDefault(x => x.UserName == userName); + + if (user is null) + { + return Result.Fail("User not found."); + } + var token = dbContext.ApiTokens.FirstOrDefault(x => x.OrganizationID == user.OrganizationID && x.ID == tokenId); + if (token is null) + { + return Result.Fail("Token not found."); + } + dbContext.ApiTokens.Remove(token); await dbContext.SaveChangesAsync(); + return Result.Ok(); } public void DeleteDeviceGroup(string orgID, string deviceGroupID) diff --git a/Shared/AppConstants.cs b/Shared/AppConstants.cs index f08b393dc..c2179fb37 100644 --- a/Shared/AppConstants.cs +++ b/Shared/AppConstants.cs @@ -4,15 +4,17 @@ using System.Text; using System.Threading.Tasks; -namespace Remotely.Shared +namespace Remotely.Shared; + +public class AppConstants { - public class AppConstants - { - public const string DefaultProductName = "Remotely"; - public const string DefaultPublisherName = "Immense Networks"; - public const int EmbeddedDataBlockLength = 256; - public const long MaxUploadFileSize = 100_000_000; - public const double ScriptRunExpirationMinutes = 30; - public static byte[] EmbeddedImmySignature { get; } = new byte[] { 73, 109, 109, 121, 66, 111, 116, 32, 114, 111, 99, 107, 115, 32, 116, 104, 101, 32, 115, 111, 99, 107, 115, 32, 117, 110, 116, 105, 108, 32, 116, 104, 101, 32, 101, 113, 117, 105, 110, 111, 120, 33 }; - } + public const string DefaultProductName = "Remotely"; + public const string DefaultPublisherName = "Immense Networks"; + public const int EmbeddedDataBlockLength = 256; + public const long MaxUploadFileSize = 100_000_000; + public const double ScriptRunExpirationMinutes = 30; + +#pragma warning disable IDE0230 // Use UTF-8 string literal + public static byte[] EmbeddedImmySignature { get; } = new byte[] { 73, 109, 109, 121, 66, 111, 116, 32, 114, 111, 99, 107, 115, 32, 116, 104, 101, 32, 115, 111, 99, 107, 115, 32, 117, 110, 116, 105, 108, 32, 116, 104, 101, 32, 101, 113, 117, 105, 110, 111, 120, 33 }; +#pragma warning restore IDE0230 // Use UTF-8 string literal } diff --git a/Shared/Extensions/DeviceExtensions.cs b/Shared/Extensions/DeviceExtensions.cs index d983ddfe9..87901f188 100644 --- a/Shared/Extensions/DeviceExtensions.cs +++ b/Shared/Extensions/DeviceExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.Serialization; using System.Text; using System.Text.Json; using System.Threading.Tasks; @@ -24,6 +25,7 @@ public static class DeviceExtensions public static DeviceClientDto ToDto(this Device device) { var json = JsonSerializer.Serialize(device, _serializerOptions); - return JsonSerializer.Deserialize(json, _serializerOptions); + var dto = JsonSerializer.Deserialize(json, _serializerOptions); + return dto ?? throw new SerializationException("Failed to create DTO."); } } diff --git a/Shared/Models/Alert.cs b/Shared/Models/Alert.cs index 18d41175d..151007577 100644 --- a/Shared/Models/Alert.cs +++ b/Shared/Models/Alert.cs @@ -9,21 +9,23 @@ public class Alert { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public string ID { get; set; } + public string ID { get; set; } = null!; + public DateTimeOffset CreatedOn { get; set; } = DateTimeOffset.Now; [JsonIgnore] - public Device Device { get; set; } - public string DeviceID { get; set; } - public string Message { get; set; } + public Device? Device { get; set; } + public string DeviceID { get; set; } = null!; + + public string? Message { get; set; } [JsonIgnore] - public Organization Organization { get; set; } + public Organization? Organization { get; set; } - public string OrganizationID { get; set; } + public string OrganizationID { get; set; } = null!; [JsonIgnore] - public RemotelyUser User { get; set; } - public string UserID { get; set; } - public string Details { get; set; } + public RemotelyUser? User { get; set; } + public string UserID { get; set; } = null!; + public string? Details { get; set; } } diff --git a/Shared/Models/AlertOptions.cs b/Shared/Models/AlertOptions.cs index 1e11c068c..1ffda6ec7 100644 --- a/Shared/Models/AlertOptions.cs +++ b/Shared/Models/AlertOptions.cs @@ -4,15 +4,15 @@ namespace Remotely.Shared.Models; public class AlertOptions { - public string AlertDeviceID { get; set; } - public string AlertMessage { get; set; } - public string ApiRequestBody { get; set; } - public Dictionary ApiRequestHeaders { get; set; } - public string ApiRequestMethod { get; set; } - public string ApiRequestUrl { get; set; } - public string EmailBody { get; set; } - public string EmailSubject { get; set; } - public string EmailTo { get; set; } + public string AlertDeviceID { get; set; } = string.Empty; + public string AlertMessage { get; set; } = string.Empty; + public string ApiRequestBody { get; set; } = string.Empty; + public Dictionary ApiRequestHeaders { get; set; } = new(); + public string ApiRequestMethod { get; set; } = string.Empty; + public string ApiRequestUrl { get; set; } = string.Empty; + public string EmailBody { get; set; } = string.Empty; + public string EmailSubject { get; set; } = string.Empty; + public string EmailTo { get; set; } = string.Empty; public bool ShouldAlert { get; set; } public bool ShouldEmail { get; set; } public bool ShouldSendApiRequest { get; set; } diff --git a/Shared/Models/ApiToken.cs b/Shared/Models/ApiToken.cs index 6435e929e..86c7448c8 100644 --- a/Shared/Models/ApiToken.cs +++ b/Shared/Models/ApiToken.cs @@ -9,16 +9,16 @@ public class ApiToken { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public string ID { get; set; } + public string ID { get; set; } = null!; public DateTimeOffset? LastUsed { get; set; } [StringLength(200)] - public string Name { get; set; } + public string? Name { get; set; } [JsonIgnore] - public Organization Organization { get; set; } + public Organization? Organization { get; set; } - public string OrganizationID { get; set; } - public string Secret { get; set; } + public string OrganizationID { get; set; } = null!; + public string? Secret { get; set; } } diff --git a/Shared/Models/ConnectionInfo.cs b/Shared/Models/ConnectionInfo.cs index 5b57e4a33..17165ac35 100644 --- a/Shared/Models/ConnectionInfo.cs +++ b/Shared/Models/ConnectionInfo.cs @@ -1,24 +1,25 @@ -using System; +#nullable enable +using System; -namespace Remotely.Shared.Models +namespace Remotely.Shared.Models; + +public class ConnectionInfo { - public class ConnectionInfo + private string? _host; + + public string DeviceID { get; set; } = Guid.NewGuid().ToString(); + public string? Host { - private string _host; - public string DeviceID { get; set; } = Guid.NewGuid().ToString(); - public string Host + get { - get - { - return _host?.Trim()?.TrimEnd('/'); - } - set - { - _host = value?.Trim()?.TrimEnd('/'); - } + return _host?.Trim()?.TrimEnd('/'); + } + set + { + _host = value?.Trim()?.TrimEnd('/') ?? string.Empty; } - public string OrganizationID { get; set; } - public string ServerVerificationToken { get; set; } - } + public string? OrganizationID { get; set; } + public string? ServerVerificationToken { get; set; } + } diff --git a/Shared/Models/Device.cs b/Shared/Models/Device.cs index 1ad5df6ff..7f5767f84 100644 --- a/Shared/Models/Device.cs +++ b/Shared/Models/Device.cs @@ -15,14 +15,14 @@ public class Device [Sortable] [Display(Name = "Agent Version")] - public string AgentVersion { get; set; } + public string? AgentVersion { get; set; } - public ICollection Alerts { get; set; } - [StringLength(100)] + public ICollection? Alerts { get; set; } + [StringLength(100)] [Sortable] [Display(Name = "Alias")] - public string Alias { get; set; } + public string? Alias { get; set; } [Sortable] [Display(Name = "CPU Utilization")] @@ -30,18 +30,18 @@ public class Device [Sortable] [Display(Name = "Current User")] - public string CurrentUser { get; set; } + public string? CurrentUser { get; set; } - public DeviceGroup DeviceGroup { get; set; } - public string DeviceGroupID { get; set; } + public DeviceGroup? DeviceGroup { get; set; } + public string? DeviceGroupID { get; set; } [Sortable] [Display(Name = "Device Name")] - public string DeviceName { get; set; } - public List Drives { get; set; } + public string? DeviceName { get; set; } + public List? Drives { get; set; } [Key] - public string ID { get; set; } + public string ID { get; set; } = null!; public bool Is64Bit { get; set; } public bool IsOnline { get; set; } @@ -55,40 +55,41 @@ public class Device public string[] MacAddresses { get; set; } = Array.Empty(); [StringLength(5000)] - public string Notes { get; set; } + public string? Notes { get; set; } [JsonIgnore] - public Organization Organization { get; set; } + public Organization? Organization { get; set; } - public string OrganizationID { get; set; } + public string OrganizationID { get; set; } = null!; public Architecture OSArchitecture { get; set; } [Sortable] [Display(Name = "OS Description")] - public string OSDescription { get; set; } + public string? OSDescription { get; set; } [Sortable] [Display(Name = "Platform")] - public string Platform { get; set; } + public string? Platform { get; set; } [Sortable] [Display(Name = "Processor Count")] public int ProcessorCount { get; set; } - public string PublicIP { get; set; } + public string? PublicIP { get; set; } [JsonIgnore] - public List ScriptResults { get; set; } + public List? ScriptResults { get; set; } [JsonIgnore] - public List ScriptRuns { get; set; } + public List? ScriptRuns { get; set; } [JsonIgnore] - public List ScriptRunsCompleted { get; set; } + public List? ScriptRunsCompleted { get; set; } [JsonIgnore] - public List ScriptSchedules { get; set; } + public List? ScriptSchedules { get; set; } + + public string? ServerVerificationToken { get; set; } - public string ServerVerificationToken { get; set; } [StringLength(200)] [Sortable] [Display(Name = "Tags")] diff --git a/Shared/Models/DeviceGroup.cs b/Shared/Models/DeviceGroup.cs index 4d18adbd8..8a5a9e49e 100644 --- a/Shared/Models/DeviceGroup.cs +++ b/Shared/Models/DeviceGroup.cs @@ -9,23 +9,23 @@ namespace Remotely.Shared.Models; public class DeviceGroup { [StringLength(200)] - public string Name { get; set; } + public required string Name { get; set; } [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public string ID { get; set; } + public string ID { get; set; } = null!; [JsonIgnore] - public List Devices { get; set; } + public List? Devices { get; set; } [JsonIgnore] - public Organization Organization { get; set; } + public Organization? Organization { get; set; } - public string OrganizationID { get; set; } + public string OrganizationID { get; set; } = null!; [JsonIgnore] - public List Users { get; set; } + public List? Users { get; set; } [JsonIgnore] - public List ScriptSchedules { get; set; } + public List? ScriptSchedules { get; set; } } diff --git a/Shared/Models/DeviceSetupOptions.cs b/Shared/Models/DeviceSetupOptions.cs index de3a0a0a2..bad2194ea 100644 --- a/Shared/Models/DeviceSetupOptions.cs +++ b/Shared/Models/DeviceSetupOptions.cs @@ -1,10 +1,10 @@ -namespace Remotely.Shared.Models +#nullable enable +namespace Remotely.Shared.Models; + +public class DeviceSetupOptions { - public class DeviceSetupOptions - { - public string DeviceAlias { get; set; } - public string DeviceGroupName { get; set; } - public string DeviceID { get; set; } - public string OrganizationID { get; set; } - } + public string? DeviceAlias { get; set; } + public string? DeviceGroupName { get; set; } + public string? DeviceID { get; set; } + public string? OrganizationID { get; set; } } diff --git a/Shared/Models/Drive.cs b/Shared/Models/Drive.cs index ac35160b7..0c5c787d2 100644 --- a/Shared/Models/Drive.cs +++ b/Shared/Models/Drive.cs @@ -5,10 +5,10 @@ namespace Remotely.Shared.Models; public class Drive { public DriveType DriveType { get; set; } - public string RootDirectory { get; set; } - public string Name { get; set; } - public string DriveFormat { get; set; } + public string? RootDirectory { get; set; } + public string? Name { get; set; } + public string? DriveFormat { get; set; } public double FreeSpace { get; set; } public double TotalSize { get; set; } - public string VolumeLabel { get; set; } + public string? VolumeLabel { get; set; } } diff --git a/Shared/Models/InviteLink.cs b/Shared/Models/InviteLink.cs index f599bc686..712dd7558 100644 --- a/Shared/Models/InviteLink.cs +++ b/Shared/Models/InviteLink.cs @@ -9,12 +9,12 @@ public class InviteLink { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public string ID { get; set; } - public string InvitedUser { get; set; } + public string ID { get; set; } = null!; + public string? InvitedUser { get; set; } public bool IsAdmin { get; set; } public DateTimeOffset DateSent { get; set; } [JsonIgnore] - public Organization Organization { get; set; } - public string OrganizationID { get; set; } - public string ResetUrl { get; set; } + public Organization? Organization { get; set; } + public string? OrganizationID { get; set; } + public string? ResetUrl { get; set; } } diff --git a/Shared/Models/Organization.cs b/Shared/Models/Organization.cs index 7f32abd93..1c5825f9e 100644 --- a/Shared/Models/Organization.cs +++ b/Shared/Models/Organization.cs @@ -9,34 +9,34 @@ namespace Remotely.Shared.Models; public class Organization { - public ICollection Alerts { get; set; } + public ICollection? Alerts { get; set; } - public ICollection ApiTokens { get; set; } + public ICollection? ApiTokens { get; set; } - public BrandingInfo BrandingInfo { get; set; } + public BrandingInfo? BrandingInfo { get; set; } - public ICollection ScriptResults { get; set; } + public ICollection? ScriptResults { get; set; } - public ICollection ScriptRuns { get; set; } - public ICollection SavedScripts { get; set; } + public ICollection? ScriptRuns { get; set; } + public ICollection? SavedScripts { get; set; } - public ICollection ScriptSchedules { get; set; } + public ICollection? ScriptSchedules { get; set; } - public ICollection DeviceGroups { get; set; } + public ICollection? DeviceGroups { get; set; } - public ICollection Devices { get; set; } + public ICollection? Devices { get; set; } [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public string ID { get; set; } + public string ID { get; set; } = null!; - public ICollection InviteLinks { get; set; } + public ICollection? InviteLinks { get; set; } public bool IsDefaultOrganization { get; set; } [StringLength(25)] - public string OrganizationName { get; set; } + public required string OrganizationName { get; set; } - public ICollection RemotelyUsers { get; set; } - public ICollection SharedFiles { get; set; } + public ICollection? RemotelyUsers { get; set; } + public ICollection? SharedFiles { get; set; } } \ No newline at end of file diff --git a/Shared/Models/RemotelyUser.cs b/Shared/Models/RemotelyUser.cs index 5c0409951..4f7a1b870 100644 --- a/Shared/Models/RemotelyUser.cs +++ b/Shared/Models/RemotelyUser.cs @@ -7,21 +7,21 @@ namespace Remotely.Shared.Models; public class RemotelyUser : IdentityUser { - public ICollection Alerts { get; set; } + public ICollection? Alerts { get; set; } - public List DeviceGroups { get; set; } + public List? DeviceGroups { get; set; } public bool IsAdministrator { get; set; } public bool IsServerAdmin { get; set; } [JsonIgnore] - public Organization Organization { get; set; } + public Organization? Organization { get; set; } - public string OrganizationID { get; set; } + public string OrganizationID { get; set; } = null!; - public List SavedScripts { get; set; } - public List ScriptSchedules { get; set; } + public List? SavedScripts { get; set; } + public List? ScriptSchedules { get; set; } - public string TempPassword { get; set; } + public string? TempPassword { get; set; } - public RemotelyUserOptions UserOptions { get; set; } + public RemotelyUserOptions? UserOptions { get; set; } } diff --git a/Shared/Models/SavedScript.cs b/Shared/Models/SavedScript.cs index c5ad867fd..9e31da0d6 100644 --- a/Shared/Models/SavedScript.cs +++ b/Shared/Models/SavedScript.cs @@ -13,15 +13,15 @@ namespace Remotely.Shared.Models; public class SavedScript { [Required] - public string Content { get; set; } + public string? Content { get; set; } [JsonIgnore] - public RemotelyUser Creator { get; set; } + public RemotelyUser? Creator { get; set; } - public string CreatorId { get; set; } + public string CreatorId { get; set; } = null!; [StringLength(200)] - public string FolderPath { get; set; } + public string? FolderPath { get; set; } public bool GenerateAlertOnError { get; set; } @@ -34,17 +34,17 @@ public class SavedScript [StringLength(100)] [Required] - public string Name { get; set; } + public required string Name { get; set; } [JsonIgnore] - public Organization Organization { get; set; } + public Organization? Organization { get; set; } - public string OrganizationID { get; set; } + public string OrganizationID { get; set; } = null!; public bool SendEmailOnError { get; set; } [EmailAddress] - public string SendErrorEmailTo { get; set; } + public string? SendErrorEmailTo { get; set; } public ScriptingShell Shell { get; set; } } diff --git a/Shared/Models/ScriptResult.cs b/Shared/Models/ScriptResult.cs index 5aaf24903..f6fc78ccf 100644 --- a/Shared/Models/ScriptResult.cs +++ b/Shared/Models/ScriptResult.cs @@ -12,32 +12,32 @@ namespace Remotely.Shared.Models; public class ScriptResult { [JsonIgnore] - public Device Device { get; set; } + public Device? Device { get; set; } - public string DeviceID { get; set; } + public string DeviceID { get; set; } = null!; - public string[] ErrorOutput { get; set; } + public string[]? ErrorOutput { get; set; } public bool HadErrors { get; set; } [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public string ID { get; set; } + public string ID { get; set; } = null!; public ScriptInputType InputType { get; set; } [JsonIgnore] [IgnoreDataMember] - public Organization Organization { get; set; } - public string OrganizationID { get; set; } + public Organization? Organization { get; set; } + public string OrganizationID { get; set; } = null!; [JsonConverter(typeof(TimeSpanJsonConverter))] public TimeSpan RunTime { get; set; } [JsonIgnore] - public ScriptSchedule Schedule { get; set; } + public ScriptSchedule? Schedule { get; set; } - public string ScriptInput { get; set; } + public required string ScriptInput { get; set; } public int? ScheduleId { get; set; } public Guid? SavedScriptId { get; set; } @@ -45,10 +45,10 @@ public class ScriptResult public int? ScriptRunId { get; set; } - public string SenderConnectionID { get; set; } - public string SenderUserName { get; set; } + public string? SenderConnectionID { get; set; } + public string? SenderUserName { get; set; } = null!; public ScriptingShell Shell { get; set; } - public string[] StandardOutput { get; set; } + public string[]? StandardOutput { get; set; } public DateTimeOffset TimeStamp { get; set; } = DateTimeOffset.Now; } diff --git a/Shared/Shared.csproj b/Shared/Shared.csproj index 7802e94d5..0976c7188 100644 --- a/Shared/Shared.csproj +++ b/Shared/Shared.csproj @@ -6,6 +6,7 @@ Remotely_Shared AnyCPU;x64;x86 Remotely.Shared + enable
diff --git a/Shared/ViewModels/ViewModelBase.cs b/Shared/ViewModels/ViewModelBase.cs index 5612b2ba5..530e95ba6 100644 --- a/Shared/ViewModels/ViewModelBase.cs +++ b/Shared/ViewModels/ViewModelBase.cs @@ -15,31 +15,32 @@ public interface IInvokePropertyChanged public class ViewModelBase : INotifyPropertyChanged, IInvokePropertyChanged { - private readonly Dictionary _propertyBackingDictionary = new(); + private readonly Dictionary _propertyBackingDictionary = new(); - public event PropertyChangedEventHandler PropertyChanged; + public event PropertyChangedEventHandler? PropertyChanged; public void InvokePropertyChanged(string propertyName = "") { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } - protected T Get([CallerMemberName] string propertyName = null) + protected T? Get([CallerMemberName] string? propertyName = null) { if (string.IsNullOrWhiteSpace(propertyName)) { throw new ArgumentNullException(nameof(propertyName)); } - if (_propertyBackingDictionary.TryGetValue(propertyName, out var value)) + if (_propertyBackingDictionary.TryGetValue(propertyName, out var value) && + value is T typedValue) { - return (T)value; + return typedValue; } - return default(T); + return default; } - protected bool Set(T newValue, [CallerMemberName] string propertyName = null) + protected bool Set(T? newValue, [CallerMemberName] string? propertyName = null) { if (string.IsNullOrWhiteSpace(propertyName)) { diff --git a/Tests/Server.Tests/DataServiceTests.cs b/Tests/Server.Tests/DataServiceTests.cs index 4d35005da..a6087342c 100644 --- a/Tests/Server.Tests/DataServiceTests.cs +++ b/Tests/Server.Tests/DataServiceTests.cs @@ -169,7 +169,8 @@ public async Task GetPendingScriptRuns_GivenMultipleRunsQueued_ReturnsOnlyLatest OrganizationID = _testData.Org1Id, SavedScriptId = savedScript.Id, ScriptRunId = scriptRun.Id, - Shell = Shared.Enums.ScriptingShell.PSCore + Shell = Shared.Enums.ScriptingShell.PSCore, + ScriptInput = "echo test" }; _dataService.AddOrUpdateScriptResult(scriptResult); @@ -199,7 +200,7 @@ public async Task TestInit() [TestMethod] public void UpdateOrganizationName() { - Assert.IsTrue(string.IsNullOrWhiteSpace(_testData.Org1Admin1.Organization.OrganizationName)); + Assert.AreEqual("Org1", _testData.Org1Admin1.Organization.OrganizationName); _dataService.UpdateOrganizationName(_testData.Org1Id, "Test Org"); var updatedOrg = _dataService.GetOrganizationById(_testData.Org1Id); Assert.AreEqual("Test Org", updatedOrg.OrganizationName); diff --git a/Tests/Server.Tests/ScriptScheduleDispatcherTests.cs b/Tests/Server.Tests/ScriptScheduleDispatcherTests.cs index bbdd6fc1a..c55234fa2 100644 --- a/Tests/Server.Tests/ScriptScheduleDispatcherTests.cs +++ b/Tests/Server.Tests/ScriptScheduleDispatcherTests.cs @@ -36,7 +36,8 @@ public async Task Init() _savedScript = new SavedScript() { - Id = Guid.NewGuid() + Id = Guid.NewGuid(), + Name = "Test Script", }; _schedule1 = new() @@ -51,6 +52,7 @@ public async Task Init() { new DeviceGroup() { + Name = "Group1", Devices = new List() { _testData.Org1Device2 diff --git a/Tests/Server.Tests/TestData.cs b/Tests/Server.Tests/TestData.cs index 2ae07b2f9..0800ef507 100644 --- a/Tests/Server.Tests/TestData.cs +++ b/Tests/Server.Tests/TestData.cs @@ -24,7 +24,7 @@ public class TestData UserName = "org1admin1@test.com", IsAdministrator = true, IsServerAdmin = true, - Organization = new Organization(), + Organization = new Organization() { OrganizationName = "Org1" }, UserOptions = new RemotelyUserOptions() }; @@ -59,7 +59,7 @@ public class TestData UserName = "org2admin1@test.com", IsAdministrator = true, IsServerAdmin = false, - Organization = new Organization(), + Organization = new Organization() { OrganizationName = "Org2" }, UserOptions = new RemotelyUserOptions() }; @@ -129,8 +129,8 @@ public async Task Init() 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 _); + await dataService.AddDeviceGroup(Org1Admin1.OrganizationID, Org1Group1); + await dataService.AddDeviceGroup(Org1Admin1.OrganizationID, Org1Group2); var deviceGroups1 = dataService.GetDeviceGroups(Org1Admin1.UserName); Org1Group1 = deviceGroups1.First(x => x.Name == Org1Group1.Name); Org1Group2 = deviceGroups1.First(x => x.Name == Org1Group2.Name); @@ -163,8 +163,8 @@ public async Task Init() 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 _); + await dataService.AddDeviceGroup(Org2Admin1.OrganizationID, Org2Group1); + await dataService.AddDeviceGroup(Org2Admin1.OrganizationID, Org2Group2); var deviceGroups2 = dataService.GetDeviceGroups(Org2Admin1.UserName); Org2Group1 = deviceGroups2.First(x => x.Name == Org2Group1.Name); Org2Group2 = deviceGroups2.First(x => x.Name == Org2Group2.Name); From b500f7e9e0b5cfbbbecfda890ec9752a30cd0137 Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Mon, 24 Jul 2023 16:11:43 -0700 Subject: [PATCH 02/29] Refactor DataService for nullability. Need to refactor calling services next. --- Agent/Services/ScriptExecutor.cs | 25 +- Directory.Build.props | 6 + Remotely.sln | 1 + Server/API/AlertsController.cs | 20 +- Server/API/ClientDownloadsController.cs | 8 +- Server/API/DevicesController.cs | 46 +- .../API/OrganizationManagementController.cs | 77 +- .../Identity/Pages/Account/Register.cshtml.cs | 26 +- .../Scripts/ScriptSchedules.razor.cs | 14 +- Server/Data/AppDb.cs | 8 +- .../Extensions/IHeaderDictionaryExtensions.cs | 28 + Server/Pages/ManageOrganization.razor.cs | 56 +- Server/Services/DataService.cs | 957 +++++++++++------- Shared/Models/Device.cs | 8 +- Shared/Models/DeviceGroup.cs | 4 +- Shared/Models/Organization.cs | 22 +- Shared/Models/RemotelyUser.cs | 8 +- Shared/Models/ScriptResult.cs | 1 + Shared/Models/ScriptRun.cs | 15 +- Shared/Models/ScriptSchedule.cs | 16 +- Shared/Models/SharedFile.cs | 12 +- Shared/Shared.csproj | 2 +- Tests/Server.Tests/DataServiceTests.cs | 6 +- 23 files changed, 864 insertions(+), 502 deletions(-) create mode 100644 Directory.Build.props create mode 100644 Server/Extensions/IHeaderDictionaryExtensions.cs diff --git a/Agent/Services/ScriptExecutor.cs b/Agent/Services/ScriptExecutor.cs index 6462f4f42..a4c91e484 100644 --- a/Agent/Services/ScriptExecutor.cs +++ b/Agent/Services/ScriptExecutor.cs @@ -107,7 +107,30 @@ public async Task RunScript(Guid savedScriptId, var url = $"{connectionInfo.Host}/API/SavedScripts/{savedScriptId}"; using var hc = new HttpClient(); hc.DefaultRequestHeaders.Add("Authorization", authToken); - var savedScript = await hc.GetFromJsonAsync(url); + var response = await hc.GetAsync(url); + if (!response.IsSuccessStatusCode) + { + if (response.StatusCode == System.Net.HttpStatusCode.NotFound) + { + return; + } + _logger.LogWarning("Failed to get saved script. Status Code: {responseStatusCode}", response.StatusCode); + return; + } + + var savedScript = await response.Content.ReadFromJsonAsync(); + + if (savedScript is null) + { + _logger.LogWarning("Failed to deserialize saved script."); + return; + } + + if (string.IsNullOrWhiteSpace(savedScript.Content)) + { + _logger.LogWarning("Script content is empty. Aborting script run."); + return; + } var result = await ExecuteScriptContent(savedScript.Shell, Guid.NewGuid().ToString(), diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 000000000..159c000cb --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,6 @@ + + + enable + + + \ No newline at end of file diff --git a/Remotely.sln b/Remotely.sln index d6f72cb50..cc6afa88a 100644 --- a/Remotely.sln +++ b/Remotely.sln @@ -39,6 +39,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{2176596E-12DA-4766-96E1-4D23EA7DBEC8}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + Directory.Build.props = Directory.Build.props LICENSE = LICENSE README.md = README.md EndProjectSection diff --git a/Server/API/AlertsController.cs b/Server/API/AlertsController.cs index 56bfa61c5..5a02b9e19 100644 --- a/Server/API/AlertsController.cs +++ b/Server/API/AlertsController.cs @@ -2,6 +2,7 @@ using Microsoft.Build.Framework; using Microsoft.Extensions.Logging; using Remotely.Server.Auth; +using Remotely.Server.Extensions; using Remotely.Server.Services; using Remotely.Shared.Models; using System; @@ -40,7 +41,10 @@ public AlertsController( [HttpPost("Create")] public async Task Create(AlertOptions alertOptions) { - _ = Request.Headers.TryGetValue("OrganizationID", out var orgId); + if (!Request.Headers.TryGetOrganizationId(out var orgId)) + { + return BadRequest("OrganizationID is required."); + } _logger.LogInformation("Alert created. Alert Options: {options}", JsonSerializer.Serialize(alertOptions)); @@ -48,7 +52,7 @@ public async Task Create(AlertOptions alertOptions) { try { - await _dataService.AddAlert(alertOptions.AlertDeviceID, orgId.ToString(), alertOptions.AlertMessage); + await _dataService.AddAlert(alertOptions.AlertDeviceID, orgId, alertOptions.AlertMessage); } catch (Exception ex) { @@ -105,11 +109,14 @@ await _emailSender.SendEmailAsync(alertOptions.EmailTo, [HttpDelete("Delete/{alertID}")] public async Task Delete(string alertID) { - Request.Headers.TryGetValue("OrganizationID", out var orgID); + if (!Request.Headers.TryGetOrganizationId(out var orgId)) + { + return BadRequest("OrganizationID is required."); + } var alert = await _dataService.GetAlert(alertID); - if (alert?.OrganizationID == orgID) + if (alert?.OrganizationID == orgId) { await _dataService.DeleteAlert(alert); @@ -122,7 +129,10 @@ public async Task Delete(string alertID) [HttpDelete("DeleteAll")] public async Task DeleteAll() { - Request.Headers.TryGetValue("OrganizationID", out var orgId); + if (!Request.Headers.TryGetOrganizationId(out var orgId)) + { + return BadRequest("OrganizationID is required."); + } if (User.Identity?.IsAuthenticated == true) { diff --git a/Server/API/ClientDownloadsController.cs b/Server/API/ClientDownloadsController.cs index 34294b751..5a11809fa 100644 --- a/Server/API/ClientDownloadsController.cs +++ b/Server/API/ClientDownloadsController.cs @@ -6,6 +6,7 @@ using Microsoft.Build.Framework; using Microsoft.Extensions.Logging; using Remotely.Server.Auth; +using Remotely.Server.Extensions; using Remotely.Server.Services; using Remotely.Shared; using Remotely.Shared.Models; @@ -117,8 +118,11 @@ public async Task GetDesktop(string platformId, string organizati [HttpGet("{platformID}")] public async Task GetInstaller(string platformID) { - Request.Headers.TryGetValue("OrganizationID", out var orgID); - return await GetInstallFile(orgID, platformID); + if (!Request.Headers.TryGetOrganizationId(out var orgId)) + { + return BadRequest("OrganizationID is required."); + } + return await GetInstallFile(orgId, platformID); } [HttpGet("{organizationID}/{platformID}")] diff --git a/Server/API/DevicesController.cs b/Server/API/DevicesController.cs index 1633a28e9..dc79cf8e7 100644 --- a/Server/API/DevicesController.cs +++ b/Server/API/DevicesController.cs @@ -1,8 +1,10 @@ using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Remotely.Server.Auth; +using Remotely.Server.Extensions; using Remotely.Server.Services; using Remotely.Shared.Models; +using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -26,28 +28,37 @@ public DevicesController(IDataService dataService) [ServiceFilter(typeof(ApiAuthorizationFilter))] public IEnumerable Get() { - Request.Headers.TryGetValue("OrganizationID", out var orgID); + if (!Request.Headers.TryGetOrganizationId(out var orgId)) + { + return Array.Empty(); + } - if (User.Identity.IsAuthenticated) + if (User.Identity?.IsAuthenticated == true && + !string.IsNullOrWhiteSpace(User.Identity.Name)) { return DataService.GetDevicesForUser(User.Identity.Name); } - return DataService.GetAllDevices(orgID); + // Authorized with API key. Return all. + return DataService.GetAllDevices(orgId); } [ServiceFilter(typeof(ApiAuthorizationFilter))] [HttpGet("{id}")] - public Device Get(string id) + public ActionResult Get(string id) { - Request.Headers.TryGetValue("OrganizationID", out var orgID); + if (!Request.Headers.TryGetOrganizationId(out var orgId)) + { + return BadRequest("OrganizationID is required."); + } - var device = DataService.GetDevice(orgID, id); + var device = DataService.GetDevice(orgId, id); - if (User.Identity.IsAuthenticated && + if (User.Identity?.IsAuthenticated == true && + !string.IsNullOrWhiteSpace(User.Identity.Name) && !DataService.DoesUserHaveAccessToDevice(id, DataService.GetUserByNameWithOrg(User.Identity.Name))) { - return null; + return Unauthorized(); } return device; } @@ -58,20 +69,31 @@ public async Task Update( [FromBody] DeviceSetupOptions deviceOptions, [FromHeader] string organizationId) { - if (deviceOptions == null || + if (string.IsNullOrWhiteSpace(deviceOptions?.DeviceID) || string.IsNullOrWhiteSpace(organizationId)) { return BadRequest("DeviceOptions and OrganizationId are required."); } - if (User.Identity.IsAuthenticated && - !DataService.DoesUserHaveAccessToDevice(deviceOptions.DeviceID, DataService.GetUserByNameWithOrg(User.Identity.Name))) + if (string.IsNullOrWhiteSpace(User.Identity?.Name)) + { + return Unauthorized(); + } + + var user = DataService.GetUserByNameWithOrg(User.Identity.Name); + if (user is null) + { + return Unauthorized(); + } + + if (User.Identity?.IsAuthenticated == true && + !DataService.DoesUserHaveAccessToDevice(deviceOptions.DeviceID, user)) { return Unauthorized(); } var device = await DataService.UpdateDevice(deviceOptions, organizationId); - if (device == null) + if (device is null) { return BadRequest(); } diff --git a/Server/API/OrganizationManagementController.cs b/Server/API/OrganizationManagementController.cs index 1487a6438..e122eb4a8 100644 --- a/Server/API/OrganizationManagementController.cs +++ b/Server/API/OrganizationManagementController.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.WebUtilities; using Org.BouncyCastle.Crypto.Agreement; using Remotely.Server.Auth; +using Remotely.Server.Extensions; using Remotely.Server.Services; using Remotely.Shared.Models; using Remotely.Shared.ViewModels; @@ -37,7 +38,14 @@ public OrganizationManagementController(IDataService dataService, [ServiceFilter(typeof(ApiAuthorizationFilter))] public IActionResult ChangeIsAdmin(string userID, [FromBody] bool isAdmin) { - if (User.Identity.IsAuthenticated && + if (User.Identity is null || + User.Identity.IsAuthenticated == false || + string.IsNullOrEmpty(User.Identity.Name)) + { + return Unauthorized(); + } + + if (User.Identity.IsAuthenticated == true && !DataService.GetUserByNameWithOrg(User.Identity.Name).IsAdministrator) { return Unauthorized(); @@ -49,25 +57,33 @@ public IActionResult ChangeIsAdmin(string userID, [FromBody] bool isAdmin) return BadRequest("You can't remove administrator rights from yourself."); } - Request.Headers.TryGetValue("OrganizationID", out var orgID); + if (!Request.Headers.TryGetOrganizationId(out var orgId)) + { + return BadRequest("OrganizationID is required."); + } - DataService.ChangeUserIsAdmin(orgID, userID, isAdmin); - return Ok("ok"); + DataService.ChangeUserIsAdmin(orgId, userID, isAdmin); + return NoContent(); } [HttpDelete("DeleteInvite/{inviteID}")] [ServiceFilter(typeof(ApiAuthorizationFilter))] public IActionResult DeleteInvite(string inviteID) { - if (User.Identity.IsAuthenticated && - !DataService.GetUserByNameWithOrg(User.Identity.Name).IsAdministrator) + if (User.Identity is null || + User.Identity.IsAuthenticated == false || + string.IsNullOrEmpty(User.Identity.Name)) { return Unauthorized(); } - Request.Headers.TryGetValue("OrganizationID", out var orgID); - DataService.DeleteInvite(orgID, inviteID); - return Ok("ok"); + if (!Request.Headers.TryGetOrganizationId(out var orgId)) + { + return BadRequest("OrganizationID is required."); + } + + DataService.DeleteInvite(orgId, inviteID); + return NoContent(); } [HttpDelete("DeleteUser/{userID}")] @@ -88,7 +104,7 @@ public async Task DeleteUser(string userID) Request.Headers.TryGetValue("OrganizationID", out var orgID); await DataService.DeleteUser(orgID, userID); - return Ok("ok"); + return NoContent(); } [HttpGet("DeviceGroup")] @@ -117,7 +133,7 @@ public IActionResult DeviceGroup([FromBody] string deviceGroupID) Request.Headers.TryGetValue("OrganizationID", out var orgID); DataService.DeleteDeviceGroup(orgID, deviceGroupID.Trim()); - return Ok("ok"); + return NoContent(); } [HttpPost("DeviceGroup")] @@ -240,7 +256,7 @@ public IActionResult Name([FromBody] string organizationName) Request.Headers.TryGetValue("OrganizationID", out var orgID); DataService.UpdateOrganizationName(orgID, organizationName.Trim()); - return Ok("ok"); + return NoContent(); } [HttpPut("SetDefault")] @@ -255,7 +271,7 @@ public IActionResult SetDefault([FromBody] bool isDefault) Request.Headers.TryGetValue("OrganizationID", out var orgID); DataService.SetIsDefaultOrganization(orgID, isDefault); - return Ok("ok"); + return NoContent(); } [HttpPost("SendInvite")] @@ -272,30 +288,41 @@ public async Task SendInvite([FromBody] InviteViewModel invite) return BadRequest(); } - Request.Headers.TryGetValue("OrganizationID", out var orgID); + if (!Request.Headers.TryGetOrganizationId(out var orgId)) + { + return BadRequest("Organization ID is required."); + } if (!DataService.DoesUserExist(invite.InvitedUser)) { - var result = await DataService.CreateUser(invite.InvitedUser, invite.IsAdmin, orgID); - if (result) + var result = await DataService.CreateUser(invite.InvitedUser, invite.IsAdmin, orgId); + if (!result.IsSuccess) { - var user = await UserManager.FindByEmailAsync(invite.InvitedUser); + return BadRequest("There was an issue creating the new account."); + } - await UserManager.ConfirmEmailAsync(user, await UserManager.GenerateEmailConfirmationTokenAsync(user)); + var user = await UserManager.FindByEmailAsync(invite.InvitedUser); - return Ok(); - } - else + if (user is null) { - return BadRequest("There was an issue creating the new account."); + return BadRequest("User not found."); } + + await UserManager.ConfirmEmailAsync(user, await UserManager.GenerateEmailConfirmationTokenAsync(user)); + + return Ok(); } else { - var newInvite = DataService.AddInvite(orgID, invite); + var newInvite = await DataService.AddInvite(orgId, invite); + + if (!newInvite.IsSuccess) + { + return BadRequest(newInvite.Reason); + } - var inviteURL = $"{Request.Scheme}://{Request.Host}/Invite?id={newInvite.ID}"; + var inviteURL = $"{Request.Scheme}://{Request.Host}/Invite?id={newInvite.Value.ID}"; var emailResult = await EmailSender.SendEmailAsync(invite.InvitedUser, "Invitation to Organization in Remotely", $@"

@@ -304,7 +331,7 @@ public async Task SendInvite([FromBody] InviteViewModel invite) You've been invited to join an organization in Remotely.

You can join the organization by clicking here.", - orgID); + orgId); if (!emailResult) { diff --git a/Server/Areas/Identity/Pages/Account/Register.cshtml.cs b/Server/Areas/Identity/Pages/Account/Register.cshtml.cs index ed8de7f64..e2d1b21f5 100644 --- a/Server/Areas/Identity/Pages/Account/Register.cshtml.cs +++ b/Server/Areas/Identity/Pages/Account/Register.cshtml.cs @@ -45,39 +45,40 @@ public RegisterModel( } [BindProperty] - public InputModel Input { get; set; } + public InputModel Input { get; set; } = null!; + public int OrganizationCount { get; set; } - public string ReturnUrl { get; set; } + public string? ReturnUrl { get; set; } - public IList ExternalLogins { get; set; } + public IList? ExternalLogins { get; set; } public class InputModel { [Required] [EmailAddress] [Display(Name = "Email")] - public string Email { get; set; } + public required string Email { get; set; } [Required] [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] [DataType(DataType.Password)] [Display(Name = "Password")] - public string Password { get; set; } + public required string Password { get; set; } [DataType(DataType.Password)] [Display(Name = "Confirm password")] [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] - public string ConfirmPassword { get; set; } + public string? ConfirmPassword { get; set; } } - public async Task OnGetAsync(string returnUrl = null) + public async Task OnGetAsync(string? returnUrl = null) { OrganizationCount = _dataService.GetOrganizationCount(); ReturnUrl = returnUrl; ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); } - public async Task OnPostAsync(string returnUrl = null) + public async Task OnPostAsync(string? returnUrl = null) { var organizationCount = _dataService.GetOrganizationCount(); if (_appConfig.MaxOrganizationCount > 0 && organizationCount >= _appConfig.MaxOrganizationCount) @@ -110,15 +111,20 @@ public async Task OnPostAsync(string returnUrl = null) var callbackUrl = Url.Page( "/Account/ConfirmEmail", pageHandler: null, - values: new { area = "Identity", userId = user.Id, code = code, returnUrl = returnUrl }, + values: new { area = "Identity", userId = user.Id, code, returnUrl }, protocol: Request.Scheme); + if (string.IsNullOrWhiteSpace(callbackUrl)) + { + return BadRequest($"{nameof(callbackUrl)} cannot be empty."); + } + await _emailSender.SendEmailAsync(Input.Email, "Confirm your email", $"Please confirm your account by clicking here."); if (_userManager.Options.SignIn.RequireConfirmedAccount) { - return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl }); + return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl }); } else { diff --git a/Server/Components/Scripts/ScriptSchedules.razor.cs b/Server/Components/Scripts/ScriptSchedules.razor.cs index 063d7399b..a9fd5af5d 100644 --- a/Server/Components/Scripts/ScriptSchedules.razor.cs +++ b/Server/Components/Scripts/ScriptSchedules.razor.cs @@ -29,7 +29,11 @@ public partial class ScriptSchedules : AuthComponentBase private SavedScript _selectedScript; - private ScriptSchedule _selectedSchedule = new() { StartAt = Time.Now }; + private ScriptSchedule _selectedSchedule = new() + { + Name = string.Empty, + StartAt = Time.Now + }; [CascadingParameter] private ScriptsPage ParentPage { get; set; } @@ -67,9 +71,13 @@ private void CreateNew() { _selectedScript = new() { - Name = "Test Script" + Name = string.Empty + }; + _selectedSchedule = new() + { + Name = string.Empty, + StartAt = Time.Now }; - _selectedSchedule = new() { StartAt = Time.Now }; _selectedDeviceGroups.Clear(); _selectedDevices.Clear(); } diff --git a/Server/Data/AppDb.cs b/Server/Data/AppDb.cs index 811641b4d..73f66437f 100644 --- a/Server/Data/AppDb.cs +++ b/Server/Data/AppDb.cs @@ -18,7 +18,7 @@ namespace Remotely.Server.Data; public class AppDb : IdentityDbContext { private static readonly ValueComparer _stringArrayComparer = new( - (a, b) => a.SequenceEqual(b), + (a, b) => (a ?? Array.Empty()).SequenceEqual(b ?? Array.Empty()), c => c.Aggregate(0, (a, b) => HashCode.Combine(a, b.GetHashCode())), c => c.ToArray()); @@ -131,9 +131,6 @@ protected override void OnModelCreating(ModelBuilder builder) builder.Entity() .HasMany(x => x.ScriptRuns) .WithMany(x => x.Devices); - builder.Entity() - .HasMany(x => x.ScriptRunsCompleted) - .WithMany(x => x.DevicesCompleted); builder.Entity() .HasMany(x => x.ScriptSchedules) .WithMany(x => x.Devices); @@ -153,9 +150,6 @@ protected override void OnModelCreating(ModelBuilder builder) builder.Entity() .HasMany(x => x.Devices) .WithMany(x => x.ScriptRuns); - builder.Entity() - .HasMany(x => x.DevicesCompleted) - .WithMany(x => x.ScriptRunsCompleted); builder.Entity() .Property(x => x.ErrorOutput) diff --git a/Server/Extensions/IHeaderDictionaryExtensions.cs b/Server/Extensions/IHeaderDictionaryExtensions.cs new file mode 100644 index 000000000..82184f35b --- /dev/null +++ b/Server/Extensions/IHeaderDictionaryExtensions.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Http; +using System.Diagnostics.CodeAnalysis; + +namespace Remotely.Server.Extensions; + +public static class IHeaderDictionaryExtensions +{ + public static bool TryGetOrganizationId( + this IHeaderDictionary headers, + [NotNullWhen(true)] out string? organizationId) + { + organizationId = null; + + if (!headers.TryGetValue("OrganizationId", out var orgId)) + { + return false; + } + + organizationId = $"{orgId}"; + + if (string.IsNullOrWhiteSpace(organizationId)) + { + return false; + } + + return true; + } +} diff --git a/Server/Pages/ManageOrganization.razor.cs b/Server/Pages/ManageOrganization.razor.cs index b783dc115..cfe604bcc 100644 --- a/Server/Pages/ManageOrganization.razor.cs +++ b/Server/Pages/ManageOrganization.razor.cs @@ -24,30 +24,30 @@ public partial class ManageOrganization : AuthComponentBase private readonly List _invites = new(); private readonly List _orgUsers = new(); private bool _inviteAsAdmin; - private string _inviteEmail; - private string _newDeviceGroupName; - private Organization _organization; - private string _selectedDeviceGroupId; + private string _inviteEmail = string.Empty; + private string _newDeviceGroupName = string.Empty; + private Organization? _organization; + private string _selectedDeviceGroupId = string.Empty; [Inject] - private IDataService DataService { get; set; } + private IDataService DataService { get; set; } = null!; [Inject] - private IEmailSenderEx EmailSender { get; set; } + private IEmailSenderEx EmailSender { get; set; } = null!; [Inject] - private IJsInterop JsInterop { get; set; } + private IJsInterop JsInterop { get; set; } = null!; [Inject] - private IModalService ModalService { get; set; } + private IModalService ModalService { get; set; } = null!; [Inject] - private NavigationManager NavManager { get; set; } + private NavigationManager NavManager { get; set; } = null!; [Inject] - private IToastService ToastService { get; set; } + private IToastService ToastService { get; set; } = null!; [Inject] - private UserManager UserManager { get; set; } + private UserManager UserManager { get; set; } = null!; protected override async Task OnInitializedAsync() @@ -88,12 +88,21 @@ private async Task CreateNewDeviceGroup() private void DefaultOrgCheckChanged(ChangeEventArgs args) { + if (_organization is null) + { + return; + } + if (!User.IsServerAdmin) { return; } - var isDefault = (bool)args.Value; + if (args.Value is not bool isDefault) + { + return; + } + DataService.SetIsDefaultOrganization(_organization.ID, isDefault); ToastService.ShowToast("Default organization set."); } @@ -194,6 +203,11 @@ private async Task EvaluateNewDeviceGroupKeyPress(KeyboardEventArgs args) } private void OrganizationNameChanged(ChangeEventArgs args) { + if (_organization is null) + { + return; + } + if (!User.IsAdministrator) { return; @@ -290,9 +304,15 @@ private async Task SendInvite() InvitedUser = _inviteEmail, IsAdmin = _inviteAsAdmin }; - var newInvite = DataService.AddInvite(User.OrganizationID, invite); + var newInvite = await DataService.AddInvite(User.OrganizationID, invite); + + if (!newInvite.IsSuccess) + { + ToastService.ShowToast($"Failed to create invite. {newInvite.Reason}", classString: "bg-danger"); + return; + } - var inviteURL = $"{NavManager.BaseUri}Invite?id={newInvite.ID}"; + var inviteURL = $"{NavManager.BaseUri}Invite?id={newInvite.Value.ID}"; var emailResult = await EmailSender.SendEmailAsync(invite.InvitedUser, "Invitation to Organization in Remotely", $@"

@@ -308,7 +328,7 @@ private async Task SendInvite() _inviteAsAdmin = false; _inviteEmail = string.Empty; - _invites.Add(newInvite); + _invites.Add(newInvite.Value); } else { @@ -324,7 +344,11 @@ private void SetUserIsAdmin(ChangeEventArgs args, RemotelyUser orgUser) return; } - var isAdmin = (bool)args.Value; + if (args.Value is not bool isAdmin) + { + return; + } + DataService.ChangeUserIsAdmin(User.OrganizationID, orgUser.Id, isAdmin); ToastService.ShowToast("Administrator value set."); } diff --git a/Server/Services/DataService.cs b/Server/Services/DataService.cs index acec9be48..237530de9 100644 --- a/Server/Services/DataService.cs +++ b/Server/Services/DataService.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; +using Microsoft.CodeAnalysis.Scripting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; @@ -25,81 +26,79 @@ namespace Remotely.Server.Services; // TODO: Separate this into domain-specific services. public interface IDataService { - Task AddAlert(string deviceID, string organizationID, string alertMessage, string? details = null); + Task AddAlert(string deviceId, string organizationId, string alertMessage, string? details = null); - Task> AddDeviceGroup(string orgID, DeviceGroup deviceGroup); + Task> AddDeviceGroup(string orgId, DeviceGroup deviceGroup); Task AddDeviceToGroup(string deviceId, string groupId); - InviteLink AddInvite(string orgID, InviteViewModel invite); + Task> AddInvite(string orgId, InviteViewModel invite); Task> AddOrUpdateDevice(DeviceClientDto device); - Task AddOrUpdateSavedScript(SavedScript script, string userId); + Task AddOrUpdateSavedScript(SavedScript script, string userId); void AddOrUpdateScriptResult(ScriptResult scriptResult); Task AddOrUpdateScriptSchedule(ScriptSchedule schedule); - Task AddScriptResultToScriptRun(string scriptResultId, int scriptRunId); + Task AddScriptResultToScriptRun(string scriptResultId, int scriptRunId); Task AddScriptRun(ScriptRun scriptRun); - Task AddSharedFile(IBrowserFile file, string organizationID, Action progressCallback); + Task AddSharedFile(IBrowserFile file, string organizationId, Action progressCallback); - Task AddSharedFile(IFormFile file, string organizationID); + Task AddSharedFile(IFormFile file, string organizationId); - bool AddUserToDeviceGroup(string orgID, string groupID, string userName, out string resultMessage); + bool AddUserToDeviceGroup(string orgId, string groupId, string userName, out string resultMessage); - void ChangeUserIsAdmin(string organizationID, string targetUserID, bool isAdmin); + void ChangeUserIsAdmin(string organizationId, string targetUserId, bool isAdmin); Task CleanupOldRecords(); - Task CreateApiToken(string userName, string tokenName, string secretHash); + Task> CreateApiToken(string userName, string tokenName, string secretHash); - Task CreateDevice(DeviceSetupOptions options); + Task> CreateDevice(DeviceSetupOptions options); - Task CreateUser(string userEmail, bool isAdmin, string organizationID); + Task CreateUser(string userEmail, bool isAdmin, string organizationId); Task DeleteAlert(Alert alert); - Task DeleteAllAlerts(string orgID, string? userName = null); + Task DeleteAllAlerts(string orgId, string? userName = null); Task DeleteApiToken(string userName, string tokenId); - void DeleteDeviceGroup(string orgID, string deviceGroupID); + Task DeleteDeviceGroup(string orgId, string deviceGroupId); - void DeleteInvite(string orgID, string inviteID); + Task DeleteInvite(string orgId, string inviteId); Task DeleteSavedScript(Guid scriptId); Task DeleteScriptSchedule(int scriptScheduleId); - Task DeleteUser(string orgID, string targetUserID); + Task DeleteUser(string orgId, string targetUserId); - void DetachEntity(object entity); - - void DeviceDisconnected(string deviceID); + void DeviceDisconnected(string deviceId); bool DoesUserExist(string userName); - bool DoesUserHaveAccessToDevice(string deviceID, RemotelyUser remotelyUser); + bool DoesUserHaveAccessToDevice(string deviceId, RemotelyUser remotelyUser); - bool DoesUserHaveAccessToDevice(string deviceID, string remotelyUserID); + bool DoesUserHaveAccessToDevice(string deviceId, string remotelyUserId); - string[] FilterDeviceIDsByUserPermission(string[] deviceIDs, RemotelyUser remotelyUser); + string[] FilterDeviceIdsByUserPermission(string[] deviceIds, RemotelyUser remotelyUser); - string[] FilterUsersByDevicePermission(IEnumerable userIDs, string deviceID); + string[] FilterUsersByDevicePermission(IEnumerable userIds, string deviceId); - Task GetAlert(string alertID); + Task> GetAlert(string alertId); - Alert[] GetAlerts(string userID); + Alert[] GetAlerts(string userId); - ApiToken[] GetAllApiTokens(string userID); + ApiToken[] GetAllApiTokens(string userId); - ScriptResult[] GetAllCommandResults(string orgID); + ScriptResult[] GetAllCommandResults(string orgId); ScriptResult[] GetAllCommandResultsForUser(string orgId, string userName, string deviceId); - Device[] GetAllDevices(string orgID); + Device[] GetAllDevices(string orgId); InviteLink[] GetAllInviteLinks(string organizationId); @@ -111,24 +110,24 @@ public interface IDataService Task GetAllUsersInOrganization(string orgId); - ApiToken GetApiKey(string keyId); + Task> GetApiKey(string keyId); - Task GetBrandingInfo(string organizationId); + Task> GetBrandingInfo(string organizationId); - Task GetDefaultOrganization(); + Task> GetDefaultOrganization(); - Device GetDevice(string deviceID); + Task> GetDevice(string deviceId); - Device GetDevice(string orgID, string deviceID); + Task> GetDevice(string orgId, string deviceId); int GetDeviceCount(); int GetDeviceCount(RemotelyUser user); - Task GetDeviceGroup( - string deviceGroupID, - bool includeDevices = false, - bool includeUsers = false); + Task> GetDeviceGroup( + string deviceGroupId, + bool includeDevices = false, + bool includeUsers = false); DeviceGroup[] GetDeviceGroups(string username); @@ -138,56 +137,56 @@ Task GetDeviceGroup( Device[] GetDevicesForUser(string userName); - Organization GetOrganizationById(string organizationID); + Task> GetOrganizationById(string organizationId); - Task GetOrganizationByUserName(string userName); + Task> GetOrganizationByUserName(string userName); int GetOrganizationCount(); Task GetOrganizationCountAsync(); - string GetOrganizationNameById(string organizationID); + Task> GetOrganizationNameById(string organizationId); - string GetOrganizationNameByUserName(string userName); + Task> GetOrganizationNameByUserName(string userName); - Task> GetPendingScriptRuns(string deviceId); + Task> GetPendingScriptRuns(string deviceId); Task> GetQuickScripts(string userId); - Task GetSavedScript(Guid scriptId); + Task> GetSavedScript(Guid scriptId); - Task GetSavedScript(string userId, Guid scriptId); + Task> GetSavedScript(string userId, Guid scriptId); Task> GetSavedScriptsWithoutContent(string userId, string organizationId); - ScriptResult GetScriptResult(string scriptResultId); + Task> GetScriptResult(string resultId); - ScriptResult GetScriptResult(string scriptResultId, string orgID); + Task> GetScriptResult(string resultId, string orgId); - Task> GetScriptSchedules(string organizationID); + Task> GetScriptSchedules(string organizationId); Task> GetScriptSchedulesDue(); List GetServerAdmins(); - SharedFile GetSharedFiled(string fileID); + Task> GetSharedFiled(string fileId); int GetTotalDevices(); - Task GetUserAsync(string username); + Task> GetUserAsync(string username); - RemotelyUser GetUserByID(string userID); + Task> GetUserById(string userId); - RemotelyUser GetUserByNameWithOrg(string userName); + Task> GetUserByNameWithOrg(string userName); - RemotelyUserOptions GetUserOptions(string userName); + Task> GetUserOptions(string userName); - bool JoinViaInvitation(string userName, string inviteID); + Task JoinViaInvitation(string userName, string inviteId); - void RemoveDevices(string[] deviceIDs); + void RemoveDevices(string[] deviceIds); - Task RemoveUserFromDeviceGroup(string orgID, string groupID, string userID); + Task RemoveUserFromDeviceGroup(string orgId, string groupId, string userId); - Task RenameApiToken(string userName, string tokenId, string tokenName); + Task RenameApiToken(string userName, string tokenId, string tokenName); Task ResetBranding(string organizationId); @@ -195,11 +194,11 @@ Task GetDeviceGroup( Task SetDisplayName(RemotelyUser user, string displayName); - Task SetIsDefaultOrganization(string orgID, bool isDefault); + Task SetIsDefaultOrganization(string orgId, bool isDefault); Task SetIsServerAdmin(string targetUserId, bool isServerAdmin, string callerUserId); - void SetServerVerificationToken(string deviceID, string verificationToken); + void SetServerVerificationToken(string deviceId, string verificationToken); Task TempPasswordSignIn(string email, string password); @@ -211,17 +210,17 @@ Task UpdateBrandingInfo( ColorPickerModel titleBackground, ColorPickerModel titleButtonForeground); - Task UpdateDevice(DeviceSetupOptions deviceOptions, string organizationId); + Task> UpdateDevice(DeviceSetupOptions deviceOptions, string organizationId); - void UpdateDevice(string deviceID, string tag, string alias, string deviceGroupID, string notes); + Task UpdateDevice(string deviceId, string tag, string alias, string deviceGroupId, string notes); - void UpdateOrganizationName(string orgID, string organizationName); + Task UpdateOrganizationName(string orgId, string newName); - void UpdateTags(string deviceID, string tags); + Task UpdateTags(string deviceID, string tags); - void UpdateUserOptions(string userName, RemotelyUserOptions options); + Task UpdateUserOptions(string userName, RemotelyUserOptions options); - bool ValidateApiKey(string keyId, string apiSecret, string requestPath, string remoteIP); + Task ValidateApiKey(string keyId, string apiSecret, string requestPath, string remoteIP); } public class DataService : IDataService @@ -243,13 +242,13 @@ public DataService( _logger = logger; } - public async Task AddAlert(string deviceId, string organizationID, string alertMessage, string? details = null) + public async Task AddAlert(string deviceId, string organizationId, string alertMessage, string? details = null) { using var dbContext = _appDbFactory.GetContext(); var users = dbContext.Users .Include(x => x.Alerts) - .Where(x => x.OrganizationID == organizationID); + .Where(x => x.OrganizationID == organizationId); if (!string.IsNullOrWhiteSpace(deviceId)) { @@ -268,22 +267,23 @@ await users.ForEachAsync(x => CreatedOn = DateTimeOffset.Now, DeviceID = deviceId, Message = alertMessage, - OrganizationID = organizationID, + OrganizationID = organizationId, Details = details }; + x.Alerts ??= new List(); x.Alerts.Add(alert); }); await dbContext.SaveChangesAsync(); } - public async Task> AddDeviceGroup(string orgID, DeviceGroup deviceGroup) + public async Task> AddDeviceGroup(string orgId, DeviceGroup deviceGroup) { using var dbContext = _appDbFactory.GetContext(); var organization = dbContext.Organizations .Include(x => x.DeviceGroups) - .FirstOrDefault(x => x.ID == orgID); + .FirstOrDefault(x => x.ID == orgId); if (organization is null) { @@ -291,7 +291,7 @@ public async Task> AddDeviceGroup(string orgID, DeviceGroup } if (dbContext.DeviceGroups.Any(x => - x.OrganizationID == orgID && + x.OrganizationID == orgId && x.Name.ToLower() == deviceGroup.Name.ToLower())) { return Result.Fail("Device group already exists."); @@ -299,8 +299,9 @@ public async Task> AddDeviceGroup(string orgID, DeviceGroup dbContext.Attach(deviceGroup); deviceGroup.Organization = organization; - deviceGroup.OrganizationID = orgID; + deviceGroup.OrganizationID = orgId; + organization.DeviceGroups ??= new List(); organization.DeviceGroups.Add(deviceGroup); await dbContext.SaveChangesAsync(); return Result.Ok(deviceGroup); @@ -334,13 +335,18 @@ public async Task AddDeviceToGroup(string deviceId, string groupId) return Result.Ok(); } - public InviteLink AddInvite(string orgID, InviteViewModel invite) + public async Task> AddInvite(string orgId, InviteViewModel invite) { using var dbContext = _appDbFactory.GetContext(); var organization = dbContext.Organizations .Include(x => x.InviteLinks) - .FirstOrDefault(x => x.ID == orgID); + .FirstOrDefault(x => x.ID == orgId); + + if (organization is null) + { + return Result.Fail("Organization not found."); + } var inviteLink = new InviteLink() { @@ -351,9 +357,10 @@ public InviteLink AddInvite(string orgID, InviteViewModel invite) OrganizationID = organization.ID, }; + organization.InviteLinks ??= new List(); organization.InviteLinks.Add(inviteLink); - dbContext.SaveChanges(); - return inviteLink; + await dbContext.SaveChangesAsync(); + return Result.Ok(inviteLink); } public async Task> AddOrUpdateDevice(DeviceClientDto deviceDto) @@ -393,9 +400,9 @@ public async Task> AddOrUpdateDevice(DeviceClientDto deviceDto) if (_hostEnvironment.IsDevelopment() && dbContext.Organizations.Any()) { - var org = await dbContext.Organizations.FirstOrDefaultAsync(); + var org = await dbContext.Organizations.FirstAsync(); device.Organization = org; - device.OrganizationID = org?.ID; + device.OrganizationID = org.ID; } if (!await dbContext.Organizations.AnyAsync(x => x.ID == device.OrganizationID)) @@ -414,15 +421,22 @@ public async Task> AddOrUpdateDevice(DeviceClientDto deviceDto) return Result.Ok(device); } - public async Task AddOrUpdateSavedScript(SavedScript script, string userId) + public async Task AddOrUpdateSavedScript(SavedScript script, string userId) { using var dbContext = _appDbFactory.GetContext(); + var user = await dbContext.Users.FindAsync(userId); + if (user is null) + { + return Result.Fail("User not found."); + } + dbContext.SavedScripts.Update(script); script.CreatorId = userId; - script.Creator = dbContext.Users.Find(userId); - script.OrganizationID = script.Creator.OrganizationID; + script.Creator = user; + script.OrganizationID = user.Id; await dbContext.SaveChangesAsync(); + return Result.Ok(); } public void AddOrUpdateScriptResult(ScriptResult result) @@ -495,33 +509,31 @@ public async Task AddOrUpdateScriptSchedule(ScriptSchedule schedule) await dbContext.SaveChangesAsync(); } - public async Task AddScriptResultToScriptRun(string scriptResultId, int scriptRunId) + public async Task AddScriptResultToScriptRun(string scriptResultId, int scriptRunId) { using var dbContext = _appDbFactory.GetContext(); var run = await dbContext.ScriptRuns .Include(x => x.Results) - .Include(x => x.DevicesCompleted) .FirstOrDefaultAsync(x => x.Id == scriptRunId); + if (run is null) + { + return Result.Fail("Run not found."); + } + var result = await dbContext.ScriptResults.FindAsync(scriptResultId); - if (run is not null && result is not null) + if (result is null) { - run.Results.Add(result); - - var device = await dbContext.Devices - .Include(x => x.ScriptRunsCompleted) - .FirstOrDefaultAsync(x => x.ID == result.DeviceID); + return Result.Fail("Results not found."); + } - if (device is not null) - { - run.DevicesCompleted.Add(device); - device.ScriptRunsCompleted.Add(run); - } + run.Results ??= new List(); + run.Results.Add(result); - await dbContext.SaveChangesAsync(); - } + await dbContext.SaveChangesAsync(); + return Result.Ok(); } public async Task AddScriptRun(ScriptRun scriptRun) @@ -533,7 +545,7 @@ public async Task AddScriptRun(ScriptRun scriptRun) await dbContext.SaveChangesAsync(); } - public async Task AddSharedFile(IBrowserFile file, string organizationID, Action progressCallback) + public async Task AddSharedFile(IBrowserFile file, string organizationId, Action progressCallback) { var fileContents = new byte[file.Size]; using var stream = file.OpenReadStream(AppConstants.MaxUploadFileSize); @@ -548,19 +560,19 @@ public async Task AddSharedFile(IBrowserFile file, string organizationID progressCallback.Invoke(1, file.Name); - return await AddSharedFileInternal(file.Name, fileContents, file.ContentType, organizationID); + return await AddSharedFileInternal(file.Name, fileContents, file.ContentType, organizationId); } - public async Task AddSharedFile(IFormFile file, string organizationID) + public async Task AddSharedFile(IFormFile file, string organizationId) { var fileContents = new byte[file.Length]; using var stream = file.OpenReadStream(); await stream.ReadAsync(fileContents.AsMemory(0, (int)file.Length)); - return await AddSharedFileInternal(file.Name, fileContents, file.ContentType, organizationID); + return await AddSharedFileInternal(file.Name, fileContents, file.ContentType, organizationId); } - public bool AddUserToDeviceGroup(string orgID, string groupID, string userName, out string resultMessage) + public bool AddUserToDeviceGroup(string orgId, string groupId, string userName, out string resultMessage) { using var dbContext = _appDbFactory.GetContext(); @@ -569,8 +581,8 @@ public bool AddUserToDeviceGroup(string orgID, string groupID, string userName, var deviceGroup = dbContext.DeviceGroups .Include(x => x.Users) .FirstOrDefault(x => - x.ID == groupID && - x.OrganizationID == orgID); + x.ID == groupId && + x.OrganizationID == orgId); if (deviceGroup == null) { @@ -583,8 +595,8 @@ public bool AddUserToDeviceGroup(string orgID, string groupID, string userName, var user = dbContext.Users .Include(x => x.DeviceGroups) .FirstOrDefault(x => - x.UserName.ToLower() == userName && - x.OrganizationID == orgID); + $"{x.UserName}".ToLower() == userName && + x.OrganizationID == orgId); if (user == null) { @@ -608,13 +620,13 @@ public bool AddUserToDeviceGroup(string orgID, string groupID, string userName, return true; } - public void ChangeUserIsAdmin(string organizationID, string targetUserID, bool isAdmin) + public void ChangeUserIsAdmin(string organizationId, string targetUserId, bool isAdmin) { using var dbContext = _appDbFactory.GetContext(); var targetUser = dbContext.Users.FirstOrDefault(x => - x.OrganizationID == organizationID && - x.Id == targetUserID); + x.OrganizationID == organizationId && + x.Id == targetUserId); if (targetUser != null) { @@ -627,46 +639,51 @@ public async Task CleanupOldRecords() { using var dbContext = _appDbFactory.GetContext(); - if (_appConfig.DataRetentionInDays > -1) + if (_appConfig.DataRetentionInDays < 0) { - var expirationDate = DateTimeOffset.Now - TimeSpan.FromDays(_appConfig.DataRetentionInDays); + return; + } - var scriptRuns = await dbContext.ScriptRuns - .Include(x => x.Results) - .Include(x => x.Devices) - .Include(x => x.DevicesCompleted) - .Where(x => x.RunAt < expirationDate) - .ToArrayAsync(); + var expirationDate = DateTimeOffset.Now - TimeSpan.FromDays(_appConfig.DataRetentionInDays); - foreach (var run in scriptRuns) - { - run.Devices?.Clear(); - run.DevicesCompleted?.Clear(); - run.Results?.Clear(); - } + var scriptRuns = await dbContext.ScriptRuns + .Include(x => x.Results) + .Include(x => x.Devices) + .Where(x => x.RunAt < expirationDate) + .ToArrayAsync(); - dbContext.RemoveRange(scriptRuns); + foreach (var run in scriptRuns) + { + run.Devices?.Clear(); + run.Results?.Clear(); + } - var commandResults = dbContext.ScriptResults - .Where(x => x.TimeStamp < expirationDate); + dbContext.RemoveRange(scriptRuns); - dbContext.RemoveRange(commandResults); + var commandResults = dbContext.ScriptResults + .Where(x => x.TimeStamp < expirationDate); - var sharedFiles = dbContext.SharedFiles - .Where(x => x.Timestamp < expirationDate); + dbContext.RemoveRange(commandResults); - dbContext.RemoveRange(sharedFiles); + var sharedFiles = dbContext.SharedFiles + .Where(x => x.Timestamp < expirationDate); - await dbContext.SaveChangesAsync(); - } + dbContext.RemoveRange(sharedFiles); + + await dbContext.SaveChangesAsync(); } - public async Task CreateApiToken(string userName, string tokenName, string secretHash) + public async Task> CreateApiToken(string userName, string tokenName, string secretHash) { using var dbContext = _appDbFactory.GetContext(); var user = dbContext.Users.FirstOrDefault(x => x.UserName == userName); + if (user is null) + { + return Result.Fail("User not found."); + } + var newToken = new ApiToken() { Name = tokenName, @@ -675,10 +692,10 @@ public async Task CreateApiToken(string userName, string tokenName, st }; dbContext.ApiTokens.Add(newToken); await dbContext.SaveChangesAsync(); - return newToken; + return Result.Ok(newToken); } - public async Task CreateDevice(DeviceSetupOptions options) + public async Task> CreateDevice(DeviceSetupOptions options) { using var dbContext = _appDbFactory.GetContext(); @@ -689,7 +706,7 @@ public async Task CreateDevice(DeviceSetupOptions options) string.IsNullOrWhiteSpace(options.OrganizationID) || dbContext.Devices.Any(x => x.ID == options.DeviceID)) { - return null; + return Result.Fail("Required parameters are missing or incorrect."); } var device = new Device() @@ -715,16 +732,16 @@ public async Task CreateDevice(DeviceSetupOptions options) await dbContext.SaveChangesAsync(); - return device; + return Result.Ok(device); } catch (Exception ex) { - _logger.LogError(ex, "error while creating device for organization {id}.", options.OrganizationID); - return null; + _logger.LogError(ex, "Error while creating device for organization {id}.", options.OrganizationID); + return Result.Fail("An error occurred while creating the device."); } } - public async Task CreateUser(string userEmail, bool isAdmin, string organizationID) + public async Task CreateUser(string userEmail, bool isAdmin, string organizationId) { using var dbContext = _appDbFactory.GetContext(); @@ -735,22 +752,28 @@ public async Task CreateUser(string userEmail, bool isAdmin, string organi UserName = userEmail.Trim().ToLower(), Email = userEmail.Trim().ToLower(), IsAdministrator = isAdmin, - OrganizationID = organizationID, + OrganizationID = organizationId, UserOptions = new RemotelyUserOptions(), LockoutEnabled = true }; var org = dbContext.Organizations .Include(x => x.RemotelyUsers) - .FirstOrDefault(x => x.ID == organizationID); + .FirstOrDefault(x => x.ID == organizationId); + + if (org is null) + { + return Result.Fail("Organization not found."); + } + dbContext.Users.Add(user); org.RemotelyUsers.Add(user); await dbContext.SaveChangesAsync(); - return true; + return Result.Ok(); } catch (Exception ex) { - _logger.LogError(ex, "Error while creating user for organization {id}.", organizationID); - return false; + _logger.LogError(ex, "Error while creating user for organization {id}.", organizationId); + return Result.Fail("An error occurred while creating user."); } } @@ -762,17 +785,21 @@ public async Task DeleteAlert(Alert alert) await dbContext.SaveChangesAsync(); } - public async Task DeleteAllAlerts(string orgID, string? userName = null) + public async Task DeleteAllAlerts(string orgId, string? userName = null) { using var dbContext = _appDbFactory.GetContext(); - var alerts = dbContext.Alerts.Where(x => x.OrganizationID == orgID); + var alerts = dbContext.Alerts.Where(x => x.OrganizationID == orgId); if (!string.IsNullOrWhiteSpace(userName)) { - var userId = GetUserByNameWithOrg(userName)?.Id; + var userResult = await GetUserByNameWithOrg(userName); - alerts = alerts.Where(x => x.UserID == userId); + if (userResult.IsSuccess) + { + var userId = userResult.Value.Id; + alerts = alerts.Where(x => x.UserID == userId); + } } dbContext.Alerts.RemoveRange(alerts); @@ -804,7 +831,7 @@ public async Task DeleteApiToken(string userName, string tokenId) return Result.Ok(); } - public void DeleteDeviceGroup(string orgID, string deviceGroupID) + public async Task DeleteDeviceGroup(string orgId, string deviceGroupID) { using var dbContext = _appDbFactory.GetContext(); @@ -814,14 +841,20 @@ public void DeleteDeviceGroup(string orgID, string deviceGroupID) .ThenInclude(x => x.DeviceGroups) .FirstOrDefault(x => x.ID == deviceGroupID && - x.OrganizationID == orgID); + x.OrganizationID == orgId); - deviceGroup.Devices?.ForEach(x => + if (deviceGroup is null) + { + return Result.Fail("Device group not found."); + } + + deviceGroup.Devices.ForEach(x => { x.DeviceGroup = null; + x.DeviceGroupID = null; }); - deviceGroup.Users?.ForEach(x => + deviceGroup.Users.ForEach(x => { x.DeviceGroups.Remove(deviceGroup); }); @@ -831,25 +864,38 @@ public void DeleteDeviceGroup(string orgID, string deviceGroupID) dbContext.DeviceGroups.Remove(deviceGroup); - dbContext.SaveChanges(); + await dbContext.SaveChangesAsync(); + return Result.Ok(); } - public void DeleteInvite(string orgID, string inviteID) + public async Task DeleteInvite(string orgId, string inviteID) { using var dbContext = _appDbFactory.GetContext(); var invite = dbContext.InviteLinks.FirstOrDefault(x => - x.OrganizationID == orgID && + x.OrganizationID == orgId && x.ID == inviteID); + if (invite is null) + { + return Result.Fail("Invite not found."); + } + var user = dbContext.Users.FirstOrDefault(x => x.UserName == invite.InvitedUser); - if (user != null && string.IsNullOrWhiteSpace(user.PasswordHash)) + if (user is null) + { + return Result.Fail("User not found."); + } + + if (string.IsNullOrWhiteSpace(user.PasswordHash)) { dbContext.Remove(user); } + dbContext.Remove(invite); - dbContext.SaveChanges(); + await dbContext.SaveChangesAsync(); + return Result.Ok(); } public async Task DeleteSavedScript(Guid scriptId) @@ -876,10 +922,20 @@ public async Task DeleteScriptSchedule(int scriptScheduleId) } } - public async Task DeleteUser(string orgID, string targetUserID) + public async Task DeleteUser(string orgId, string targetUserId) { using var dbContext = _appDbFactory.GetContext(); + var org = dbContext + .Organizations + .Include(x => x.RemotelyUsers) + .FirstOrDefault(x => x.ID == orgId); + + if (org is null) + { + return Result.Fail("Organization not found."); + } + var target = dbContext.Users .Include(x => x.DeviceGroups) .ThenInclude(x => x.Devices) @@ -888,20 +944,17 @@ public async Task DeleteUser(string orgID, string targetUserID) .Include(x => x.SavedScripts) .Include(x => x.ScriptSchedules) .FirstOrDefault(x => - x.Id == targetUserID && - x.OrganizationID == orgID); + x.Id == targetUserId && + x.OrganizationID == orgId); if (target is null) { - return; + return Result.Fail("User not found."); } - if (target.DeviceGroups?.Any() == true) + foreach (var deviceGroup in target.DeviceGroups) { - foreach (var deviceGroup in target.DeviceGroups.ToList()) - { - deviceGroup.Users.Remove(target); - } + deviceGroup.Users.Remove(target); } foreach (var alert in target.Alerts) @@ -909,32 +962,19 @@ public async Task DeleteUser(string orgID, string targetUserID) dbContext.Alerts.Remove(alert); } - target.OrganizationID = null; target.Organization = null; - - dbContext - .Organizations - .Include(x => x.RemotelyUsers) - .FirstOrDefault(x => x.ID == orgID) - .RemotelyUsers.Remove(target); - + org.RemotelyUsers.Remove(target); dbContext.Users.Remove(target); await dbContext.SaveChangesAsync(); + return Result.Ok(); } - public void DetachEntity(object entity) - { - using var dbContext = _appDbFactory.GetContext(); - - dbContext.Entry(entity).State = EntityState.Detached; - } - - public void DeviceDisconnected(string deviceID) + public void DeviceDisconnected(string deviceId) { using var dbContext = _appDbFactory.GetContext(); - var device = dbContext.Devices.Find(deviceID); + var device = dbContext.Devices.Find(deviceId); if (device != null) { device.LastOnline = DateTimeOffset.Now; @@ -951,10 +991,11 @@ public bool DoesUserExist(string userName) { return false; } - return dbContext.Users.Any(x => x.UserName.Trim().ToLower() == userName.Trim().ToLower()); + + return dbContext.Users.Any(x => $"{x.UserName}".Trim().ToLower() == userName.Trim().ToLower()); } - public bool DoesUserHaveAccessToDevice(string deviceID, RemotelyUser remotelyUser) + public bool DoesUserHaveAccessToDevice(string deviceId, RemotelyUser remotelyUser) { if (remotelyUser is null) { @@ -965,76 +1006,94 @@ public bool DoesUserHaveAccessToDevice(string deviceID, RemotelyUser remotelyUse return dbContext.Devices .Include(x => x.DeviceGroup) - .ThenInclude(x => x.Users) - .Any(device => device.OrganizationID == remotelyUser.OrganizationID && - device.ID == deviceID && + .ThenInclude(x => x!.Users) + .Any(device => + device.OrganizationID == remotelyUser.OrganizationID && + device.ID == deviceId && ( remotelyUser.IsAdministrator || - device.DeviceGroup.Users.Any(user => user.Id == remotelyUser.Id + device.DeviceGroup!.Users.Any(user => user.Id == remotelyUser.Id ))); } - public bool DoesUserHaveAccessToDevice(string deviceID, string remotelyUserID) + public bool DoesUserHaveAccessToDevice(string deviceId, string remotelyUserId) { using var dbContext = _appDbFactory.GetContext(); - var remotelyUser = dbContext.Users.Find(remotelyUserID); + var remotelyUser = dbContext.Users.Find(remotelyUserId); - return DoesUserHaveAccessToDevice(deviceID, remotelyUser); + if (remotelyUser is null) + { + return false; + } + + return DoesUserHaveAccessToDevice(deviceId, remotelyUser); } - public string[] FilterDeviceIDsByUserPermission(string[] deviceIDs, RemotelyUser remotelyUser) + public string[] FilterDeviceIdsByUserPermission(string[] deviceIds, RemotelyUser remotelyUser) { using var dbContext = _appDbFactory.GetContext(); return dbContext.Devices .Include(x => x.DeviceGroup) - .ThenInclude(x => x.Users) + .ThenInclude(x => x!.Users) .Where(device => device.OrganizationID == remotelyUser.OrganizationID && - deviceIDs.Contains(device.ID) && + deviceIds.Contains(device.ID) && ( remotelyUser.IsAdministrator || - device.DeviceGroup.Users.Any(user => user.Id == remotelyUser.Id + device.DeviceGroup!.Users.Any(user => user.Id == remotelyUser.Id ))) .Select(x => x.ID) .ToArray(); } - public string[] FilterUsersByDevicePermission(IEnumerable userIDs, string deviceID) + public string[] FilterUsersByDevicePermission(IEnumerable userIds, string deviceId) { using var dbContext = _appDbFactory.GetContext(); - return FilterUsersByDevicePermissionInternal(dbContext, userIDs, deviceID); + return FilterUsersByDevicePermissionInternal(dbContext, userIds, deviceId); } - public async Task GetAlert(string alertID) + public async Task> GetAlert(string alertId) { using var dbContext = _appDbFactory.GetContext(); - return await dbContext.Alerts + var alert = await dbContext.Alerts .Include(x => x.Device) .Include(x => x.User) - .FirstOrDefaultAsync(x => x.ID == alertID); + .FirstOrDefaultAsync(x => x.ID == alertId); + + if (alert is null) + { + return Result.Fail("Alert not found."); + } + + return Result.Ok(alert); } - public Alert[] GetAlerts(string userID) + public Alert[] GetAlerts(string userId) { using var dbContext = _appDbFactory.GetContext(); return dbContext.Alerts .Include(x => x.Device) .Include(x => x.User) - .Where(x => x.UserID == userID) + .Where(x => x.UserID == userId) .OrderByDescending(x => x.CreatedOn) .ToArray(); } - public ApiToken[] GetAllApiTokens(string userID) + public ApiToken[] GetAllApiTokens(string userId) { using var dbContext = _appDbFactory.GetContext(); - var user = dbContext.Users.FirstOrDefault(x => x.Id == userID); + var user = dbContext.Users.FirstOrDefault(x => x.Id == userId); + + if (user is null) + { + return Array.Empty(); + } return dbContext.ApiTokens .Where(x => x.OrganizationID == user.OrganizationID) @@ -1042,12 +1101,12 @@ public ApiToken[] GetAllApiTokens(string userID) .ToArray(); } - public ScriptResult[] GetAllCommandResults(string orgID) + public ScriptResult[] GetAllCommandResults(string orgId) { using var dbContext = _appDbFactory.GetContext(); return dbContext.ScriptResults - .Where(x => x.OrganizationID == orgID) + .Where(x => x.OrganizationID == orgId) .OrderByDescending(x => x.TimeStamp) .ToArray(); } @@ -1064,11 +1123,11 @@ public ScriptResult[] GetAllCommandResultsForUser(string orgId, string userName, .ToArray(); } - public Device[] GetAllDevices(string orgID) + public Device[] GetAllDevices(string orgId) { using var dbContext = _appDbFactory.GetContext(); - return dbContext.Devices.Where(x => x.OrganizationID == orgID).ToArray(); + return dbContext.Devices.Where(x => x.OrganizationID == orgId).ToArray(); } public InviteLink[] GetAllInviteLinks(string organizationId) @@ -1120,26 +1179,38 @@ public async Task GetAllUsersInOrganization(string orgId) .Include(x => x.RemotelyUsers) .FirstOrDefaultAsync(x => x.ID == orgId); + if (organization is null) + { + return Array.Empty(); + } + return organization.RemotelyUsers.ToArray(); } - public ApiToken GetApiKey(string keyId) + public async Task> GetApiKey(string keyId) { if (string.IsNullOrWhiteSpace(keyId)) { - return null; + return Result.Fail("Key ID cannot be empty."); } using var dbContext = _appDbFactory.GetContext(); - return dbContext.ApiTokens.FirstOrDefault(x => x.ID == keyId); + var token = await dbContext.ApiTokens.FirstOrDefaultAsync(x => x.ID == keyId); + + if (token is null) + { + return Result.Fail("API key not found."); + } + + return Result.Ok(token); } - public async Task GetBrandingInfo(string organizationId) + public async Task> GetBrandingInfo(string organizationId) { if (string.IsNullOrWhiteSpace(organizationId)) { - return null; + return Result.Fail("Organization ID cannot be empty."); } using var dbContext = _appDbFactory.GetContext(); @@ -1150,7 +1221,7 @@ public async Task GetBrandingInfo(string organizationId) if (organization is null) { - return null; + return Result.Fail("Organization not found."); } if (organization.BrandingInfo is null) @@ -1165,30 +1236,49 @@ public async Task GetBrandingInfo(string organizationId) await dbContext.SaveChangesAsync(); } - return organization.BrandingInfo; + return Result.Ok(organization.BrandingInfo); } - public async Task GetDefaultOrganization() + public async Task> GetDefaultOrganization() { using var dbContext = _appDbFactory.GetContext(); - return await dbContext.Organizations.FirstOrDefaultAsync(x => x.IsDefaultOrganization); + var org = await dbContext.Organizations.FirstOrDefaultAsync(x => x.IsDefaultOrganization); + + if (org is null) + { + return Result.Fail("Organization not found."); + } + + return Result.Ok(org); } - public Device GetDevice(string orgID, string deviceID) + public async Task> GetDevice(string orgId, string deviceId) { using var dbContext = _appDbFactory.GetContext(); - return dbContext.Devices.FirstOrDefault(x => - x.OrganizationID == orgID && - x.ID == deviceID); + var device = await dbContext.Devices.FirstOrDefaultAsync(x => + x.OrganizationID == orgId && + x.ID == deviceId); + + if (device is null) + { + return Result.Fail("Device not found."); + } + return Result.Ok(device); } - public Device GetDevice(string deviceID) + public async Task> GetDevice(string deviceId) { using var dbContext = _appDbFactory.GetContext(); - return dbContext.Devices.FirstOrDefault(x => x.ID == deviceID); + var device = await dbContext.Devices.FirstOrDefaultAsync(x => x.ID == deviceId); + + if (device is null) + { + return Result.Fail("Device not found."); + } + return Result.Ok(device); } public int GetDeviceCount() @@ -1216,8 +1306,8 @@ public int GetDeviceCount(RemotelyUser user) .Count(); } - public async Task GetDeviceGroup( - string deviceGroupID, + public async Task> GetDeviceGroup( + string deviceGroupId, bool includeDevices = false, bool includeUsers = false) { @@ -1234,7 +1324,13 @@ public async Task GetDeviceGroup( query = query.Include(x => x.Users); } - return await query.FirstOrDefaultAsync(x => x.ID == deviceGroupID); + var group = await query.FirstOrDefaultAsync(x => x.ID == deviceGroupId); + + if (group is null) + { + return Result.Fail("Device group not found."); + } + return Result.Ok(group); } public DeviceGroup[] GetDeviceGroups(string username) @@ -1245,7 +1341,7 @@ public DeviceGroup[] GetDeviceGroups(string username) if (user is null) { - return null; + return Array.Empty(); } var userId = user.Id; @@ -1259,7 +1355,8 @@ public DeviceGroup[] GetDeviceGroups(string username) x.Users.Any(x => x.Id == userId) ) ) - .Select(x => x.ID); + .Select(x => x.ID) + .ToHashSet(); if (groupIds.Any()) { @@ -1329,24 +1426,35 @@ public Device[] GetDevicesForUser(string userName) .ToArray(); } - public Organization GetOrganizationById(string organizationID) + public async Task> GetOrganizationById(string organizationId) { using var dbContext = _appDbFactory.GetContext(); - return dbContext.Organizations.Find(organizationID); + var org = await dbContext.Organizations.FindAsync(organizationId); + + if (org is null) + { + return Result.Fail("Organization not found."); + } + return Result.Ok(org); } - public async Task GetOrganizationByUserName(string userName) + public async Task> GetOrganizationByUserName(string userName) { using var dbContext = _appDbFactory.GetContext(); var user = await dbContext .Users .Include(x => x.Organization) - .FirstOrDefaultAsync(x => x.UserName.ToLower() == userName.ToLower()); + .FirstOrDefaultAsync(x => x.UserName!.ToLower() == userName.ToLower()); + + if (user?.Organization is null) + { + return Result.Fail("User not found."); + } - return user.Organization; + return Result.Ok(user.Organization); } public int GetOrganizationCount() @@ -1363,58 +1471,62 @@ public async Task GetOrganizationCountAsync() return await dbContext.Organizations.CountAsync(); } - public string GetOrganizationNameById(string organizationID) + public async Task> GetOrganizationNameById(string organizationId) { using var dbContext = _appDbFactory.GetContext(); - return dbContext.Organizations.FirstOrDefault(x => x.ID == organizationID)?.OrganizationName; + var org = await dbContext.Organizations.FirstOrDefaultAsync(x => x.ID == organizationId); + + if (org is null) + { + return Result.Fail("Organization not found."); + } + + return Result.Ok(org.OrganizationName); } - public string GetOrganizationNameByUserName(string userName) + public async Task> GetOrganizationNameByUserName(string userName) { using var dbContext = _appDbFactory.GetContext(); - return dbContext.Users + var user = await dbContext.Users .Include(x => x.Organization) - .FirstOrDefault(x => x.UserName == userName) - .Organization - .OrganizationName; + .FirstOrDefaultAsync(x => x.UserName == userName); + + if (user is null) + { + return Result.Fail("User not found."); + } + + var orgName = $"{user.Organization?.OrganizationName}"; + return Result.Ok(orgName); } - public async Task> GetPendingScriptRuns(string deviceId) + public async Task> GetPendingScriptRuns(string deviceId) { using var dbContext = _appDbFactory.GetContext(); - var pendingRuns = new List(); - - var now = Time.Now; + var device = await dbContext.Devices + .Include(x => x.ScriptRuns) + .ThenInclude(x => x.Results) + .FirstOrDefaultAsync(x => x.ID == deviceId); - var scriptRunGroups = dbContext.ScriptRuns - .Include(x => x.Devices) - .Include(x => x.DevicesCompleted) - .Where(scriptRun => - scriptRun.RunOnNextConnect && - dbContext.SavedScripts.Any(savedScript => savedScript.Id == scriptRun.SavedScriptId) && - scriptRun.Devices.Any(device => device.ID == deviceId) && - scriptRun.RunAt < now) - .AsEnumerable() - .GroupBy(x => x.SavedScriptId); - - foreach (var group in scriptRunGroups) - { - var latestRun = group - .OrderByDescending(x => x.RunAt) - .FirstOrDefault(); - - if (!latestRun.DevicesCompleted.Any(x => x.ID == deviceId)) - { - pendingRuns.Add(latestRun); - } + if (device is null) + { + return Enumerable.Empty(); } - await dbContext.SaveChangesAsync(); + device.ScriptResults ??= new(); + var scriptResultsLookup = device.ScriptResults + .Select(x => x.ScriptRunId) + .Distinct() + .ToHashSet(); - return pendingRuns; + return device.ScriptRuns + .OrderByDescending(x => x.RunAt) + .DistinctBy(x => x.SavedScriptId) + .Where(x => !scriptResultsLookup.Contains(x.Id)) + .ToArray(); } public async Task> GetQuickScripts(string userId) @@ -1426,60 +1538,85 @@ public async Task> GetQuickScripts(string userId) .ToListAsync(); } - public async Task GetSavedScript(string userId, Guid scriptId) + public async Task> GetSavedScript(string userId, Guid scriptId) { using var dbContext = _appDbFactory.GetContext(); - return await dbContext.SavedScripts + var script = await dbContext.SavedScripts .Include(x => x.Creator) .FirstOrDefaultAsync(x => x.Id == scriptId && (x.IsPublic || x.CreatorId == userId)); + + if (script is null) + { + return Result.Fail("Script not found."); + } + return Result.Ok(script); } - public async Task GetSavedScript(Guid scriptId) + public async Task> GetSavedScript(Guid scriptId) { using var dbContext = _appDbFactory.GetContext(); - return await dbContext.SavedScripts.FirstOrDefaultAsync(x => x.Id == scriptId); + var script = await dbContext.SavedScripts.FirstOrDefaultAsync(x => x.Id == scriptId); + + if (script is null) + { + return Result.Fail("Script not found."); + } + return Result.Ok(script); } public async Task> GetSavedScriptsWithoutContent(string userId, string organizationId) { using var dbContext = _appDbFactory.GetContext(); - var query = dbContext.SavedScripts + return await dbContext.SavedScripts .Include(x => x.Creator) - .Where(x => x.Creator.OrganizationID == organizationId && - (x.IsPublic || x.CreatorId == userId)); - - return await query.Select(x => new SavedScript() - { - Creator = x.Creator, - CreatorId = x.CreatorId, - FolderPath = x.FolderPath, - Id = x.Id, - IsPublic = x.IsPublic, - IsQuickScript = x.IsQuickScript, - Name = x.Name, - OrganizationID = x.OrganizationID - }).ToListAsync(); + .Where(x => + x.Creator!.OrganizationID == organizationId && + (x.IsPublic || x.CreatorId == userId)) + .Select(x => new SavedScript() + { + Creator = x.Creator, + CreatorId = x.CreatorId, + FolderPath = x.FolderPath, + Id = x.Id, + IsPublic = x.IsPublic, + IsQuickScript = x.IsQuickScript, + Name = x.Name, + OrganizationID = x.OrganizationID + }) + .ToListAsync(); } - public ScriptResult GetScriptResult(string commandResultID, string orgID) + public async Task> GetScriptResult(string resultId, string orgId) { using var dbContext = _appDbFactory.GetContext(); - return dbContext.ScriptResults - .FirstOrDefault(x => - x.OrganizationID == orgID && - x.ID == commandResultID); + var result = await dbContext.ScriptResults + .FirstOrDefaultAsync(x => + x.OrganizationID == orgId && + x.ID == resultId); + + if (result is null) + { + return Result.Fail("Script result not found."); + } + return Result.Ok(result); } - public ScriptResult GetScriptResult(string commandResultID) + public async Task> GetScriptResult(string resultId) { using var dbContext = _appDbFactory.GetContext(); - return dbContext.ScriptResults.Find(commandResultID); + var result = await dbContext.ScriptResults.FindAsync(resultId); + + if (result is null) + { + return Result.Fail("Script result not found."); + } + return Result.Ok(result); } public async Task> GetScriptSchedules(string organizationId) @@ -1513,15 +1650,21 @@ public List GetServerAdmins() return dbContext.Users .Where(x => x.IsServerAdmin) - .Select(x => x.UserName) + .Select(x => $"{x.UserName}") .ToList(); } - public SharedFile GetSharedFiled(string fileID) + public async Task> GetSharedFiled(string fileId) { using var dbContext = _appDbFactory.GetContext(); - return dbContext.SharedFiles.Find(fileID); + var file = await dbContext.SharedFiles.FindAsync(fileId); + + if (file is null) + { + return Result.Fail("File not found."); + } + return Result.Ok(file); } public int GetTotalDevices() @@ -1531,79 +1674,112 @@ public int GetTotalDevices() return dbContext.Devices.Count(); } - public async Task GetUserAsync(string username) + public async Task> GetUserAsync(string username) { if (string.IsNullOrWhiteSpace(username)) { - return null; + return Result.Fail("Username cannot be empty."); } using var dbContext = _appDbFactory.GetContext(); - return await dbContext.Users.FirstOrDefaultAsync(x => x.UserName == username); + var user = await dbContext.Users.FirstOrDefaultAsync(x => x.UserName == username); + + if (user is null) + { + return Result.Fail("User not found."); + } + return Result.Ok(user); } - public RemotelyUser GetUserByID(string userID) + public async Task> GetUserById(string userId) { - if (string.IsNullOrWhiteSpace(userID)) + if (string.IsNullOrWhiteSpace(userId)) { - return null; + return Result.Fail("User ID cannot be empty."); } using var dbContext = _appDbFactory.GetContext(); - return dbContext.Users.FirstOrDefault(x => x.Id == userID); + var user = await dbContext.Users.FirstOrDefaultAsync(x => x.Id == userId); + + if (user is null) + { + return Result.Fail("User not found."); + } + return Result.Ok(user); } - public RemotelyUser GetUserByNameWithOrg(string userName) + public async Task> GetUserByNameWithOrg(string userName) { if (userName == null) { - return null; + return Result.Fail("Username cannot be empty."); } using var dbContext = _appDbFactory.GetContext(); - return dbContext.Users + var user = await dbContext.Users .Include(x => x.Organization) - .FirstOrDefault(x => x.UserName.ToLower().Trim() == userName.ToLower().Trim()); + .FirstOrDefaultAsync(x => x.UserName!.ToLower().Trim() == userName.ToLower().Trim()); + + if (user is null) + { + return Result.Fail("User not found."); + } + return Result.Ok(user); } - public RemotelyUserOptions GetUserOptions(string userName) + public async Task> GetUserOptions(string userName) { using var dbContext = _appDbFactory.GetContext(); - return dbContext.Users - .FirstOrDefault(x => x.UserName == userName) - .UserOptions; + var user = await dbContext.Users + .FirstOrDefaultAsync(x => x.UserName == userName); + + if (user is null) + { + return Result.Fail("User not found."); + } + return Result.Ok(user.UserOptions ?? new()); } - public bool JoinViaInvitation(string userName, string inviteID) + public async Task JoinViaInvitation(string userName, string inviteId) { using var dbContext = _appDbFactory.GetContext(); - var invite = dbContext.InviteLinks.FirstOrDefault(x => - x.InvitedUser.ToLower() == userName.ToLower() && - x.ID == inviteID); + var invite = await dbContext.InviteLinks.FirstOrDefaultAsync(x => + x.InvitedUser!.ToLower() == userName.ToLower() && + x.ID == inviteId); - if (invite == null) + if (invite is null) { - return false; + return Result.Fail("Invite not found."); } - var user = dbContext.Users.FirstOrDefault(x => x.UserName == userName); - var organization = dbContext.Organizations + var user = await dbContext.Users.FirstOrDefaultAsync(x => x.UserName == userName); + if (user is null) + { + return Result.Fail("User not found."); + } + + var organization = await dbContext.Organizations .Include(x => x.RemotelyUsers) - .FirstOrDefault(x => x.ID == invite.OrganizationID); + .FirstOrDefaultAsync(x => x.ID == invite.OrganizationID); + + if (organization is null) + { + return Result.Fail("Organization not found."); + } user.Organization = organization; user.OrganizationID = organization.ID; user.IsAdministrator = invite.IsAdmin; organization.RemotelyUsers.Add(user); - dbContext.SaveChanges(); + await dbContext.SaveChangesAsync(); dbContext.InviteLinks.Remove(invite); dbContext.SaveChanges(); - return true; + return Result.Ok(); } public void RemoveDevices(string[] deviceIDs) @@ -1614,7 +1790,6 @@ public void RemoveDevices(string[] deviceIDs) .Include(x => x.ScriptResults) .Include(x => x.ScriptRuns) .Include(x => x.ScriptSchedules) - .Include(x => x.ScriptRunsCompleted) .Include(x => x.DeviceGroup) .Include(x => x.Alerts) .Where(x => deviceIDs.Contains(x.ID)); @@ -1627,37 +1802,54 @@ public async Task RemoveUserFromDeviceGroup(string orgID, string groupID, { using var dbContext = _appDbFactory.GetContext(); - var deviceGroup = dbContext.DeviceGroups + var deviceGroup = await dbContext.DeviceGroups .Include(x => x.Users) .ThenInclude(x => x.DeviceGroups) - .FirstOrDefault(x => + .FirstOrDefaultAsync(x => x.ID == groupID && x.OrganizationID == orgID); - if (deviceGroup?.Users?.Any(x => x.Id == userID) == true) + if (deviceGroup?.Users?.Any(x => x.Id == userID) != true) { - var user = deviceGroup.Users.FirstOrDefault(x => x.Id == userID); + return false; + } - user.DeviceGroups.Remove(deviceGroup); - deviceGroup.Users.Remove(user); + var user = deviceGroup.Users.FirstOrDefault(x => x.Id == userID); - await dbContext.SaveChangesAsync(); - return true; + if (user is null) + { + return false; } - return false; + + user.DeviceGroups.Remove(deviceGroup); + deviceGroup.Users.Remove(user); + + await dbContext.SaveChangesAsync(); + return true; } - public async Task RenameApiToken(string userName, string tokenId, string tokenName) + public async Task RenameApiToken(string userName, string tokenId, string tokenName) { using var dbContext = _appDbFactory.GetContext(); - var user = dbContext.Users.FirstOrDefault(x => x.UserName == userName); - var token = dbContext.ApiTokens.FirstOrDefault(x => + var user = await dbContext.Users.FirstOrDefaultAsync(x => x.UserName == userName); + if (user is null) + { + return Result.Fail("User not found."); + } + + var token = await dbContext.ApiTokens.FirstOrDefaultAsync(x => x.OrganizationID == user.OrganizationID && x.ID == tokenId); + if (token is null) + { + return Result.Fail("API token not found."); + } + token.Name = tokenName; await dbContext.SaveChangesAsync(); + return Result.Ok(); } public async Task ResetBranding(string organizationId) @@ -1695,6 +1887,7 @@ public async Task SetDisplayName(RemotelyUser user, string displayName) using var dbContext = _appDbFactory.GetContext(); dbContext.Attach(user); + user.UserOptions ??= new(); user.UserOptions.DisplayName = displayName; await dbContext.SaveChangesAsync(); } @@ -1764,22 +1957,25 @@ public async Task TempPasswordSignIn(string email, string password) return false; } - var user = GetUserByNameWithOrg(email); + var userResult = await GetUserByNameWithOrg(email); - if (user is null) + if (!userResult.IsSuccess) { return false; } + + var user = userResult.Value; + using var dbContext = _appDbFactory.GetContext(); - if (user.TempPassword == password) + if (user.TempPassword != password) { - user.TempPassword = string.Empty; - await dbContext.SaveChangesAsync(); - return true; + return false; } - return false; + user.TempPassword = string.Empty; + await dbContext.SaveChangesAsync(); + return true; } public async Task UpdateBrandingInfo( @@ -1825,101 +2021,116 @@ public async Task UpdateBrandingInfo( await dbContext.SaveChangesAsync(); } - public void UpdateDevice(string deviceID, string tag, string alias, string deviceGroupID, string notes) + public async Task UpdateDevice(string deviceId, string tag, string alias, string deviceGroupId, string notes) { using var dbContext = _appDbFactory.GetContext(); - var device = dbContext.Devices + var device = await dbContext.Devices .Include(x => x.DeviceGroup) - .FirstOrDefault(x => x.ID == deviceID); - if (device == null) + .FirstOrDefaultAsync(x => x.ID == deviceId); + + if (device is null) { return; } - if (string.IsNullOrWhiteSpace(deviceGroupID)) + if (string.IsNullOrWhiteSpace(deviceGroupId)) { - device.DeviceGroup?.Devices?.RemoveAll(x => x.ID == deviceID); + device.DeviceGroup?.Devices?.RemoveAll(x => x.ID == deviceId); device.DeviceGroup = null; device.DeviceGroupID = null; } else { - device.DeviceGroupID = deviceGroupID; + device.DeviceGroupID = deviceGroupId; } device.Tags = tag; device.Alias = alias; device.Notes = notes; - dbContext.SaveChanges(); + await dbContext.SaveChangesAsync(); } - public async Task UpdateDevice(DeviceSetupOptions deviceOptions, string organizationId) + public async Task> UpdateDevice(DeviceSetupOptions deviceOptions, string organizationId) { using var dbContext = _appDbFactory.GetContext(); - var device = dbContext.Devices.Find(deviceOptions.DeviceID); + var device = await dbContext.Devices.FindAsync(deviceOptions.DeviceID); if (device == null || device.OrganizationID != organizationId) { - return null; + return Result.Fail("Device not found."); } var group = await dbContext.DeviceGroups.FirstOrDefaultAsync(x => - x.Name.ToLower() == deviceOptions.DeviceGroupName.ToLower() && + x.Name.ToLower() == $"{deviceOptions.DeviceGroupName}".ToLower() && x.OrganizationID == device.OrganizationID); device.DeviceGroup = group; device.Alias = deviceOptions.DeviceAlias; await dbContext.SaveChangesAsync(); - return device; + return Result.Ok(device); } - public void UpdateOrganizationName(string orgID, string organizationName) + public async Task UpdateOrganizationName(string orgId, string newName) { using var dbContext = _appDbFactory.GetContext(); - dbContext.Organizations - .FirstOrDefault(x => x.ID == orgID) - .OrganizationName = organizationName; - dbContext.SaveChanges(); + var org = await dbContext.Organizations.FirstOrDefaultAsync(x => x.ID == orgId); + + if (org is null) + { + return Result.Fail("Organization not found."); + } + + org.OrganizationName = newName; + await dbContext.SaveChangesAsync(); + return Result.Ok(); } - public void UpdateTags(string deviceID, string tags) + public async Task UpdateTags(string deviceID, string tags) { using var dbContext = _appDbFactory.GetContext(); - var device = dbContext.Devices.Find(deviceID); + var device = await dbContext.Devices.FindAsync(deviceID); if (device == null) { return; } device.Tags = tags; - dbContext.SaveChanges(); + await dbContext.SaveChangesAsync(); } - public void UpdateUserOptions(string userName, RemotelyUserOptions options) + public async Task UpdateUserOptions(string userName, RemotelyUserOptions options) { using var dbContext = _appDbFactory.GetContext(); - dbContext.Users.FirstOrDefault(x => x.UserName == userName).UserOptions = options; - dbContext.SaveChanges(); + var user = await dbContext.Users.FirstOrDefaultAsync(x => x.UserName == userName); + + if (user is null) + { + return Result.Fail("User not found."); + } + user.UserOptions = options; + await dbContext.SaveChangesAsync(); + return Result.Ok(); } - public bool ValidateApiKey(string keyId, string apiSecret, string requestPath, string remoteIP) + public async Task ValidateApiKey(string keyId, string apiSecret, string requestPath, string remoteIP) { using var dbContext = _appDbFactory.GetContext(); - var hasher = new PasswordHasher(); - var token = dbContext.ApiTokens.FirstOrDefault(x => x.ID == keyId); + var hasher = new PasswordHasher(); + var token = await dbContext.ApiTokens.FirstOrDefaultAsync(x => x.ID == keyId); - var isValid = token is not null && - hasher.VerifyHashedPassword(null, token.Secret, apiSecret) == PasswordVerificationResult.Success; + var isValid = + !string.IsNullOrWhiteSpace(token?.Secret) && + hasher.VerifyHashedPassword(string.Empty, token.Secret, apiSecret) == PasswordVerificationResult.Success; if (token is not null) { token.LastUsed = DateTimeOffset.Now; - dbContext.SaveChanges(); + await dbContext.SaveChangesAsync(); } _logger.LogInformation( @@ -1962,7 +2173,7 @@ private string[] FilterUsersByDevicePermissionInternal(AppDb dbContext, IEnumera { var device = dbContext.Devices .Include(x => x.DeviceGroup) - .ThenInclude(x => x.Users) + .ThenInclude(x => x!.Users) .FirstOrDefault(x => x.ID == deviceID); if (device is null) diff --git a/Shared/Models/Device.cs b/Shared/Models/Device.cs index 7f5767f84..87aced45d 100644 --- a/Shared/Models/Device.cs +++ b/Shared/Models/Device.cs @@ -76,17 +76,15 @@ public class Device public int ProcessorCount { get; set; } public string? PublicIP { get; set; } - [JsonIgnore] - public List? ScriptResults { get; set; } [JsonIgnore] - public List? ScriptRuns { get; set; } + public List ScriptResults { get; set; } = new(); [JsonIgnore] - public List? ScriptRunsCompleted { get; set; } + public List ScriptRuns { get; set; } = new(); [JsonIgnore] - public List? ScriptSchedules { get; set; } + public List ScriptSchedules { get; set; } = new(); public string? ServerVerificationToken { get; set; } diff --git a/Shared/Models/DeviceGroup.cs b/Shared/Models/DeviceGroup.cs index 8a5a9e49e..611d4d305 100644 --- a/Shared/Models/DeviceGroup.cs +++ b/Shared/Models/DeviceGroup.cs @@ -16,7 +16,7 @@ public class DeviceGroup public string ID { get; set; } = null!; [JsonIgnore] - public List? Devices { get; set; } + public List Devices { get; set; } = new(); [JsonIgnore] public Organization? Organization { get; set; } @@ -24,7 +24,7 @@ public class DeviceGroup public string OrganizationID { get; set; } = null!; [JsonIgnore] - public List? Users { get; set; } + public List Users { get; set; } = new(); [JsonIgnore] public List? ScriptSchedules { get; set; } diff --git a/Shared/Models/Organization.cs b/Shared/Models/Organization.cs index 1c5825f9e..610226c7e 100644 --- a/Shared/Models/Organization.cs +++ b/Shared/Models/Organization.cs @@ -9,34 +9,34 @@ namespace Remotely.Shared.Models; public class Organization { - public ICollection? Alerts { get; set; } + public ICollection Alerts { get; set; } = new List(); - public ICollection? ApiTokens { get; set; } + public ICollection ApiTokens { get; set; } = new List(); public BrandingInfo? BrandingInfo { get; set; } - public ICollection? ScriptResults { get; set; } + public ICollection ScriptResults { get; set; } = new List(); - public ICollection? ScriptRuns { get; set; } - public ICollection? SavedScripts { get; set; } + public ICollection ScriptRuns { get; set; } = new List(); + public ICollection SavedScripts { get; set; } = new List(); - public ICollection? ScriptSchedules { get; set; } + public ICollection ScriptSchedules { get; set; } = new List(); - public ICollection? DeviceGroups { get; set; } + public ICollection DeviceGroups { get; set; } = new List(); - public ICollection? Devices { get; set; } + public ICollection Devices { get; set; } = new List(); [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public string ID { get; set; } = null!; - public ICollection? InviteLinks { get; set; } + public ICollection InviteLinks { get; set; } = new List(); public bool IsDefaultOrganization { get; set; } [StringLength(25)] public required string OrganizationName { get; set; } - public ICollection? RemotelyUsers { get; set; } - public ICollection? SharedFiles { get; set; } + public ICollection RemotelyUsers { get; set; } = new List(); + public ICollection SharedFiles { get; set; } = new List(); } \ No newline at end of file diff --git a/Shared/Models/RemotelyUser.cs b/Shared/Models/RemotelyUser.cs index 4f7a1b870..53c863c6b 100644 --- a/Shared/Models/RemotelyUser.cs +++ b/Shared/Models/RemotelyUser.cs @@ -7,9 +7,9 @@ namespace Remotely.Shared.Models; public class RemotelyUser : IdentityUser { - public ICollection? Alerts { get; set; } + public ICollection Alerts { get; set; } = new List(); - public List? DeviceGroups { get; set; } + public List DeviceGroups { get; set; } = new(); public bool IsAdministrator { get; set; } public bool IsServerAdmin { get; set; } @@ -18,8 +18,8 @@ public class RemotelyUser : IdentityUser public string OrganizationID { get; set; } = null!; - public List? SavedScripts { get; set; } - public List? ScriptSchedules { get; set; } + public List SavedScripts { get; set; } = new(); + public List ScriptSchedules { get; set; } = new(); public string? TempPassword { get; set; } diff --git a/Shared/Models/ScriptResult.cs b/Shared/Models/ScriptResult.cs index f6fc78ccf..8d6844260 100644 --- a/Shared/Models/ScriptResult.cs +++ b/Shared/Models/ScriptResult.cs @@ -31,6 +31,7 @@ public class ScriptResult [IgnoreDataMember] public Organization? Organization { get; set; } public string OrganizationID { get; set; } = null!; + [JsonConverter(typeof(TimeSpanJsonConverter))] public TimeSpan RunTime { get; set; } diff --git a/Shared/Models/ScriptRun.cs b/Shared/Models/ScriptRun.cs index a84182600..929dcfc6b 100644 --- a/Shared/Models/ScriptRun.cs +++ b/Shared/Models/ScriptRun.cs @@ -13,23 +13,22 @@ namespace Remotely.Shared.Models; public class ScriptRun { [JsonIgnore] - public List Devices { get; set; } - - [JsonIgnore] - public List DevicesCompleted { get; set; } + public List? Devices { get; set; } [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } - public string Initiator { get; set; } + + public string? Initiator { get; set; } public ScriptInputType InputType { get; set; } [JsonIgnore] - public Organization Organization { get; set; } + public Organization? Organization { get; set; } + + public string OrganizationID { get; set; } = null!; - public string OrganizationID { get; set; } [JsonIgnore] - public List Results { get; set; } + public List? Results { get; set; } public DateTimeOffset RunAt { get; set; } public bool RunOnNextConnect { get; set; } diff --git a/Shared/Models/ScriptSchedule.cs b/Shared/Models/ScriptSchedule.cs index 84fb52fe2..71cf7e8cb 100644 --- a/Shared/Models/ScriptSchedule.cs +++ b/Shared/Models/ScriptSchedule.cs @@ -16,15 +16,15 @@ public class ScriptSchedule public DateTimeOffset CreatedAt { get; set; } [JsonIgnore] - public RemotelyUser Creator { get; set; } + public RemotelyUser? Creator { get; set; } - public string CreatorId { get; set; } + public string CreatorId { get; set; } = null!; [JsonIgnore] - public List DeviceGroups { get; set; } + public List DeviceGroups { get; set; } = new(); [JsonIgnore] - public List Devices { get; set; } + public List Devices { get; set; } = new(); [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } @@ -33,19 +33,19 @@ public class ScriptSchedule public DateTimeOffset? LastRun { get; set; } - public string Name { get; set; } + public required string Name { get; set; } public DateTimeOffset NextRun { get; set; } public DateTimeOffset StartAt { get; set; } [JsonIgnore] - public Organization Organization { get; set; } - public string OrganizationID { get; set; } + public Organization? Organization { get; set; } + public string OrganizationID { get; set; } = null!; public bool RunOnNextConnect { get; set; } = true; public Guid SavedScriptId { get; set; } [JsonIgnore] - public List ScriptRuns { get; set; } + public List ScriptRuns { get; set; } = new(); } diff --git a/Shared/Models/SharedFile.cs b/Shared/Models/SharedFile.cs index 03f813b5a..7f26502e5 100644 --- a/Shared/Models/SharedFile.cs +++ b/Shared/Models/SharedFile.cs @@ -8,11 +8,11 @@ public class SharedFile { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public string ID { get; set; } - public string FileName { get; set; } - public string ContentType { get; set; } - public byte[] FileContents { get; set; } + public string ID { get; set; } = null!; + public string? FileName { get; set; } + public string? ContentType { get; set; } + public byte[] FileContents { get; set; } = Array.Empty(); public DateTimeOffset Timestamp { get; set; } = DateTimeOffset.Now; - public Organization Organization { get; set; } - public string OrganizationID { get; set; } + public Organization? Organization { get; set; } + public string OrganizationID { get; set; } = null!; } diff --git a/Shared/Shared.csproj b/Shared/Shared.csproj index 0976c7188..182ada766 100644 --- a/Shared/Shared.csproj +++ b/Shared/Shared.csproj @@ -3,7 +3,7 @@ net7.0 true - Remotely_Shared + AnyCPU;x64;x86 Remotely.Shared enable diff --git a/Tests/Server.Tests/DataServiceTests.cs b/Tests/Server.Tests/DataServiceTests.cs index a6087342c..2b55839f3 100644 --- a/Tests/Server.Tests/DataServiceTests.cs +++ b/Tests/Server.Tests/DataServiceTests.cs @@ -159,8 +159,8 @@ public async Task GetPendingScriptRuns_GivenMultipleRunsQueued_ReturnsOnlyLatest var pendingRuns = await _dataService.GetPendingScriptRuns(_testData.Org1Device1.ID); - Assert.AreEqual(1, pendingRuns.Count); - Assert.AreEqual(2, pendingRuns[0].Id); + Assert.AreEqual(1, pendingRuns.Count()); + Assert.AreEqual(2, pendingRuns.First().Id); var scriptResult = new ScriptResult() { @@ -179,7 +179,7 @@ public async Task GetPendingScriptRuns_GivenMultipleRunsQueued_ReturnsOnlyLatest pendingRuns = await _dataService.GetPendingScriptRuns(_testData.Org1Device1.ID); - Assert.AreEqual(0, pendingRuns.Count); + Assert.AreEqual(0, pendingRuns.Count()); } From 8b341562bc47b1c8cc029b8a0f866d493d3bdfdc Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Tue, 25 Jul 2023 07:18:34 -0700 Subject: [PATCH 03/29] Refactor OrganizationManagementController, ApiAuthorizationFilter, and ExpiringTokenFilter. --- Agent/Services/AgentHubConnection.cs | 5 +- Agent/Services/ScriptExecutor.cs | 10 +- README.md | 6 +- Server/API/AlertsController.cs | 25 ++- Server/API/ClientDownloadsController.cs | 2 +- Server/API/DevicesController.cs | 2 +- .../API/OrganizationManagementController.cs | 198 +++++++++--------- Server/API/RemoteControlController.cs | 35 +++- Server/API/ScriptResultsController.cs | 64 ++++-- Server/Auth/ApiAuthorizationFilter.cs | 90 ++++---- Server/Auth/ExpiringTokenFilter.cs | 76 ++++--- .../Extensions/IHeaderDictionaryExtensions.cs | 8 + Server/Pages/ApiKeys.razor | 17 +- Server/Services/AuthService.cs | 11 +- Server/Services/DataService.cs | 19 +- Server/Shared/NavMenu.razor | 29 ++- Shared/AppConstants.cs | 2 + submodules/Immense.RemoteControl | 2 +- 18 files changed, 368 insertions(+), 233 deletions(-) diff --git a/Agent/Services/AgentHubConnection.cs b/Agent/Services/AgentHubConnection.cs index 93189df01..97aa999fe 100644 --- a/Agent/Services/AgentHubConnection.cs +++ b/Agent/Services/AgentHubConnection.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging; using Remotely.Agent.Extensions; using Remotely.Agent.Interfaces; +using Remotely.Shared; using Remotely.Shared.Enums; using Remotely.Shared.Models; using Remotely.Shared.Services; @@ -449,7 +450,7 @@ private void RegisterMessageHandlers() } }); - _hubConnection.On("TransferFileFromBrowserToAgent", async (string transferID, List fileIDs, string requesterID, string authToken) => + _hubConnection.On("TransferFileFromBrowserToAgent", async (string transferID, List fileIDs, string requesterID, string expiringToken) => { try { @@ -467,7 +468,7 @@ private void RegisterMessageHandlers() { var url = $"{_connectionInfo.Host}/API/FileSharing/{fileID}"; using var client = _httpFactory.CreateClient(); - client.DefaultRequestHeaders.Add("Authorization", authToken); + client.DefaultRequestHeaders.Add(AppConstants.ExpiringTokenHeaderName, expiringToken); using var response = await client.GetAsync(url); var filename = response.Content.Headers.ContentDisposition.FileName; diff --git a/Agent/Services/ScriptExecutor.cs b/Agent/Services/ScriptExecutor.cs index a4c91e484..71ca180d7 100644 --- a/Agent/Services/ScriptExecutor.cs +++ b/Agent/Services/ScriptExecutor.cs @@ -93,7 +93,7 @@ public async Task RunScript(Guid savedScriptId, int scriptRunId, string initiator, ScriptInputType scriptInputType, - string authToken) + string expiringToken) { try { @@ -106,7 +106,7 @@ public async Task RunScript(Guid savedScriptId, var connectionInfo = _configService.GetConnectionInfo(); var url = $"{connectionInfo.Host}/API/SavedScripts/{savedScriptId}"; using var hc = new HttpClient(); - hc.DefaultRequestHeaders.Add("Authorization", authToken); + hc.DefaultRequestHeaders.Add(AppConstants.ExpiringTokenHeaderName, expiringToken); var response = await hc.GetAsync(url); if (!response.IsSuccessStatusCode) { @@ -142,7 +142,7 @@ public async Task RunScript(Guid savedScriptId, result.InputType = scriptInputType; result.SavedScriptId = savedScriptId; - var responseResult = await SendResultsToApi(result, authToken); + var responseResult = await SendResultsToApi(result, expiringToken); } catch (Exception ex) { @@ -195,12 +195,12 @@ private async Task ExecuteScriptContent( } return null; } - private async Task SendResultsToApi(object result, string authToken) + private async Task SendResultsToApi(object result, string expiringToken) { var targetURL = _configService.GetConnectionInfo().Host + $"/API/ScriptResults"; using var httpClient = new HttpClient(); - httpClient.DefaultRequestHeaders.Add("Authorization", authToken); + httpClient.DefaultRequestHeaders.Add(AppConstants.ExpiringTokenHeaderName, expiringToken); using var response = await httpClient.PostAsJsonAsync(targetURL, result); diff --git a/README.md b/README.md index 7d8c77aff..4a9b14472 100644 --- a/README.md +++ b/README.md @@ -207,13 +207,13 @@ Remotely has a basic API, which can be browsed at https://remotely.lucency.co/sw When accessing the API from the browser on another website, you'll need to set up CORS in appsettings by adding the website origin URL to the TrustedCorsOrigins array. If you're not familiar with how CORS works, I recommend reading up on it before proceeding. For example, if I wanted to create a login form on https://lucency.co that logged into the Remotely API, I'd need to add "https://lucency.co" to the TrustedCorsOrigins. -The API key and secret must first be combined [ApiKey]:[ApiSecret] and then encoded with Base64 as [EncodedAuhorization]. After that you can add the encoded string to the request's Authorization header in the form "Basic [EncodedAuhorization]" +Each request to the API must have a header named "X-Api-Key". The value should be the API key's ID and secret, separated by a colon (i.e. [ApiKey]:[ApiSecret]). Below is an example API request: POST https://localhost:5001/API/Scripting/ExecuteCommand/PSCore/f2b0a595-5ea8-471b-975f-12e70e0f3497 HTTP/1.1 Content-Type: application/json - Authorization: 31fb288d-af97-4ce1-ae7b-ceebb98281ac:HLkrKaZGExYvozSPvcACZw9awKkhHnNK + X-Api-Key: 31fb288d-af97-4ce1-ae7b-ceebb98281ac:HLkrKaZGExYvozSPvcACZw9awKkhHnNK User-Agent: PostmanRuntime/7.22.0 Accept: */* Cache-Control: no-cache @@ -284,7 +284,7 @@ Register-ScheduledJob -ScriptBlock { $FreeSpace = $OsDrive.Free / ($OsDrive.Used + $OsDrive.Free) if ($FreeSpace -lt .1) { Invoke-WebRequest -Uri "https://localhost:5001/api/Alerts/Create/" -Method Post -Headers @{ - Authorization="3e9d8273-1dc1-4303-bd50-7a133e36b9b7:S+82XKZdvg278pSFHWtUklqHENuO5IhH" + X-Api-Key="3e9d8273-1dc1-4303-bd50-7a133e36b9b7:S+82XKZdvg278pSFHWtUklqHENuO5IhH" } -Body @" { "AlertDeviceID": "f2b0a595-5ea8-471b-975f-12e70e0f3497", diff --git a/Server/API/AlertsController.cs b/Server/API/AlertsController.cs index 5a02b9e19..680b98b05 100644 --- a/Server/API/AlertsController.cs +++ b/Server/API/AlertsController.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using Immense.RemoteControl.Shared.Extensions; +using Microsoft.AspNetCore.Mvc; using Microsoft.Build.Framework; using Microsoft.Extensions.Logging; using Remotely.Server.Auth; @@ -43,7 +44,7 @@ public async Task Create(AlertOptions alertOptions) { if (!Request.Headers.TryGetOrganizationId(out var orgId)) { - return BadRequest("OrganizationID is required."); + return Unauthorized(); } _logger.LogInformation("Alert created. Alert Options: {options}", JsonSerializer.Serialize(alertOptions)); @@ -111,19 +112,23 @@ public async Task Delete(string alertID) { if (!Request.Headers.TryGetOrganizationId(out var orgId)) { - return BadRequest("OrganizationID is required."); + return Unauthorized(); } - var alert = await _dataService.GetAlert(alertID); - - if (alert?.OrganizationID == orgId) + var alertResult = await _dataService.GetAlert(alertID); + _logger.LogResult(alertResult); + if (!alertResult.IsSuccess) { - await _dataService.DeleteAlert(alert); + return BadRequest(alertResult.Reason); + } - return Ok(); + if (alertResult.Value.OrganizationID != orgId) + { + return Unauthorized(); } - return Unauthorized(); + await _dataService.DeleteAlert(alertResult.Value); + return Ok(); } [HttpDelete("DeleteAll")] @@ -131,7 +136,7 @@ public async Task DeleteAll() { if (!Request.Headers.TryGetOrganizationId(out var orgId)) { - return BadRequest("OrganizationID is required."); + return Unauthorized(); } if (User.Identity?.IsAuthenticated == true) diff --git a/Server/API/ClientDownloadsController.cs b/Server/API/ClientDownloadsController.cs index 5a11809fa..8d7f580b9 100644 --- a/Server/API/ClientDownloadsController.cs +++ b/Server/API/ClientDownloadsController.cs @@ -120,7 +120,7 @@ public async Task GetInstaller(string platformID) { if (!Request.Headers.TryGetOrganizationId(out var orgId)) { - return BadRequest("OrganizationID is required."); + return Unauthorized(); } return await GetInstallFile(orgId, platformID); } diff --git a/Server/API/DevicesController.cs b/Server/API/DevicesController.cs index dc79cf8e7..f963bb06c 100644 --- a/Server/API/DevicesController.cs +++ b/Server/API/DevicesController.cs @@ -49,7 +49,7 @@ public ActionResult Get(string id) { if (!Request.Headers.TryGetOrganizationId(out var orgId)) { - return BadRequest("OrganizationID is required."); + return Unauthorized(); } var device = DataService.GetDevice(orgId, id); diff --git a/Server/API/OrganizationManagementController.cs b/Server/API/OrganizationManagementController.cs index e122eb4a8..84d7562ee 100644 --- a/Server/API/OrganizationManagementController.cs +++ b/Server/API/OrganizationManagementController.cs @@ -1,13 +1,19 @@ -using MailKit; +using Immense.RemoteControl.Desktop.Native.Windows; +using Immense.RemoteControl.Shared; +using Immense.RemoteControl.Shared.Extensions; +using MailKit; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Build.Framework; +using Microsoft.Extensions.Logging; using Org.BouncyCastle.Crypto.Agreement; using Remotely.Server.Auth; using Remotely.Server.Extensions; using Remotely.Server.Services; using Remotely.Shared.Models; using Remotely.Shared.ViewModels; +using System; using System.Text; using System.Text.Encodings.Web; using System.Threading.Tasks; @@ -20,90 +26,91 @@ namespace Remotely.Server.API; [ApiController] public class OrganizationManagementController : ControllerBase { - public OrganizationManagementController(IDataService dataService, + private readonly IDataService _dataService; + private readonly IEmailSenderEx _emailSender; + private readonly ILogger _logger; + private readonly UserManager _userManager; + + public OrganizationManagementController( UserManager userManager, - IEmailSenderEx emailSender) + IDataService dataService, + IEmailSenderEx emailSender, + ILogger logger) { - DataService = dataService; - UserManager = userManager; - EmailSender = emailSender; + _dataService = dataService; + _userManager = userManager; + _emailSender = emailSender; + _logger = logger; } - private IDataService DataService { get; } - private IEmailSenderEx EmailSender { get; } - private UserManager UserManager { get; } - - [HttpPost("ChangeIsAdmin/{userID}")] [ServiceFilter(typeof(ApiAuthorizationFilter))] - public IActionResult ChangeIsAdmin(string userID, [FromBody] bool isAdmin) + public async Task ChangeIsAdmin(string userId, [FromBody] bool isAdmin) { - if (User.Identity is null || - User.Identity.IsAuthenticated == false || - string.IsNullOrEmpty(User.Identity.Name)) - { - return Unauthorized(); - } - - if (User.Identity.IsAuthenticated == true && - !DataService.GetUserByNameWithOrg(User.Identity.Name).IsAdministrator) + if (!Request.Headers.TryGetOrganizationId(out var orgId)) { return Unauthorized(); } - if (User.Identity.IsAuthenticated && - DataService.GetUserByNameWithOrg(User.Identity.Name).Id == userID) + if (User.Identity?.IsAuthenticated == true) { - return BadRequest("You can't remove administrator rights from yourself."); - } - - if (!Request.Headers.TryGetOrganizationId(out var orgId)) - { - return BadRequest("OrganizationID is required."); + var userResult = await _dataService.GetUserByNameWithOrg($"{User.Identity.Name}"); + if (userResult.IsSuccess && userResult.Value.Id == userId) + { + return BadRequest("You can't remove administrator rights from yourself."); + } } - DataService.ChangeUserIsAdmin(orgId, userID, isAdmin); + _dataService.ChangeUserIsAdmin(orgId, userId, isAdmin); return NoContent(); } [HttpDelete("DeleteInvite/{inviteID}")] [ServiceFilter(typeof(ApiAuthorizationFilter))] - public IActionResult DeleteInvite(string inviteID) + public async Task DeleteInvite(string inviteID) { - if (User.Identity is null || - User.Identity.IsAuthenticated == false || - string.IsNullOrEmpty(User.Identity.Name)) + if (!Request.Headers.TryGetOrganizationId(out var orgId)) { return Unauthorized(); } - if (!Request.Headers.TryGetOrganizationId(out var orgId)) + var result = await _dataService.DeleteInvite(orgId, inviteID); + _logger.LogResult(result); + + if (!result.IsSuccess) { - return BadRequest("OrganizationID is required."); + return BadRequest(result.Reason); } - DataService.DeleteInvite(orgId, inviteID); return NoContent(); } [HttpDelete("DeleteUser/{userID}")] [ServiceFilter(typeof(ApiAuthorizationFilter))] - public async Task DeleteUser(string userID) + public async Task DeleteUser(string userId) { - if (User.Identity.IsAuthenticated && - !DataService.GetUserByNameWithOrg(User.Identity.Name).IsAdministrator) + if (!Request.Headers.TryGetOrganizationId(out var orgId)) { return Unauthorized(); } - if (User.Identity.IsAuthenticated && - DataService.GetUserByNameWithOrg(User.Identity.Name).Id == userID) + if (User.Identity?.IsAuthenticated == true) + { + var userResult = await _dataService.GetUserByNameWithOrg($"{User.Identity.Name}"); + if (userResult.IsSuccess && userResult.Value.Id == userId) + { + return BadRequest("You can't delete yourself here. You must go to the Personal Data page to delete your own account."); + } + } + + + var result = await _dataService.DeleteUser(orgId, userId); + _logger.LogResult(result); + if (!result.IsSuccess) { - return BadRequest("You can't delete yourself here. You must go to the Personal Data page to delete your own account."); + return BadRequest(result.Reason); } - Request.Headers.TryGetValue("OrganizationID", out var orgID); - await DataService.DeleteUser(orgID, userID); return NoContent(); } @@ -111,28 +118,29 @@ public async Task DeleteUser(string userID) [ServiceFilter(typeof(ApiAuthorizationFilter))] public IActionResult DeviceGroup() { - if (User.Identity.IsAuthenticated && - DataService.GetUserByNameWithOrg(User.Identity.Name).IsAdministrator) + if (!Request.Headers.TryGetOrganizationId(out var orgId)) { - return Ok(DataService.GetDeviceGroups(User.Identity.Name)); + return Unauthorized(); } - if (Request.Headers.TryGetValue("OrganizationID", out var orgID)) - return Ok(DataService.GetDeviceGroupsForOrganization(orgID)); - return NotFound("Unable to find User or organizationID for device group"); + + return Ok(_dataService.GetDeviceGroupsForOrganization(orgId)); } [HttpDelete("DeviceGroup")] [ServiceFilter(typeof(ApiAuthorizationFilter))] - public IActionResult DeviceGroup([FromBody] string deviceGroupID) + public async Task DeviceGroup([FromBody] string deviceGroupId) { - if (User.Identity.IsAuthenticated && - !DataService.GetUserByNameWithOrg(User.Identity.Name).IsAdministrator) + if (!Request.Headers.TryGetOrganizationId(out var orgId)) { return Unauthorized(); } - Request.Headers.TryGetValue("OrganizationID", out var orgID); - DataService.DeleteDeviceGroup(orgID, deviceGroupID.Trim()); + var result = await _dataService.DeleteDeviceGroup(orgId, deviceGroupId.Trim()); + _logger.LogResult(result); + if (!result.IsSuccess) + { + return BadRequest(result.Reason); + } return NoContent(); } @@ -140,8 +148,7 @@ public IActionResult DeviceGroup([FromBody] string deviceGroupID) [ServiceFilter(typeof(ApiAuthorizationFilter))] public async Task DeviceGroup([FromBody] DeviceGroup deviceGroup) { - if (User.Identity.IsAuthenticated && - !DataService.GetUserByNameWithOrg(User.Identity.Name).IsAdministrator) + if (!Request.Headers.TryGetOrganizationId(out var orgId)) { return Unauthorized(); } @@ -151,9 +158,7 @@ public async Task DeviceGroup([FromBody] DeviceGroup deviceGroup) return BadRequest(); } - Request.Headers.TryGetValue("OrganizationID", out var orgId); - - var result = await DataService.AddDeviceGroup($"{orgId}", deviceGroup); + var result = await _dataService.AddDeviceGroup(orgId, deviceGroup); if (!result.IsSuccess) { return BadRequest(result.Reason); @@ -165,8 +170,7 @@ public async Task DeviceGroup([FromBody] DeviceGroup deviceGroup) [ServiceFilter(typeof(ApiAuthorizationFilter))] public async Task DeviceGroupRemoveUser([FromBody] string userID, string groupID) { - if (User.Identity.IsAuthenticated && - !DataService.GetUserByNameWithOrg(User.Identity.Name).IsAdministrator) + if (!Request.Headers.TryGetOrganizationId(out var orgId)) { return Unauthorized(); } @@ -176,8 +180,7 @@ public async Task DeviceGroupRemoveUser([FromBody] string userID, return BadRequest(); } - Request.Headers.TryGetValue("OrganizationID", out var orgID); - if (!await DataService.RemoveUserFromDeviceGroup(orgID, groupID, userID)) + if (!await _dataService.RemoveUserFromDeviceGroup(orgId, groupID, userID)) { return BadRequest("Failed to remove user from group."); } @@ -188,8 +191,7 @@ public async Task DeviceGroupRemoveUser([FromBody] string userID, [ServiceFilter(typeof(ApiAuthorizationFilter))] public IActionResult DeviceGroupAddUser([FromBody] string userID, string groupID) { - if (User.Identity.IsAuthenticated && - !DataService.GetUserByNameWithOrg(User.Identity.Name).IsAdministrator) + if (!Request.Headers.TryGetOrganizationId(out var orgId)) { return Unauthorized(); } @@ -199,8 +201,7 @@ public IActionResult DeviceGroupAddUser([FromBody] string userID, string groupID return BadRequest(); } - Request.Headers.TryGetValue("OrganizationID", out var orgID); - var result = DataService.AddUserToDeviceGroup(orgID, groupID, userID, out var resultMessage); + var result = _dataService.AddUserToDeviceGroup(orgId, groupID, userID, out var resultMessage); if (!result) { return BadRequest(resultMessage); @@ -211,24 +212,26 @@ public IActionResult DeviceGroupAddUser([FromBody] string userID, string groupID [HttpGet("GenerateResetUrl/{userID}")] [ServiceFilter(typeof(ApiAuthorizationFilter))] - public async Task GenerateResetUrl(string userID) + public async Task GenerateResetUrl(string userId) { - if (User.Identity.IsAuthenticated && - !DataService.GetUserByNameWithOrg(User.Identity.Name).IsAdministrator) + if (!Request.Headers.TryGetOrganizationId(out var orgId)) { return Unauthorized(); } - Request.Headers.TryGetValue("OrganizationID", out var orgID); + var user = await _userManager.FindByIdAsync(userId); - var user = await UserManager.FindByIdAsync(userID); + if (user is null) + { + return NotFound(); + } - if (user.OrganizationID != orgID) + if (user.OrganizationID != orgId) { return Unauthorized(); } - var code = await UserManager.GeneratePasswordResetTokenAsync(user); + var code = await _userManager.GeneratePasswordResetTokenAsync(user); code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); var callbackUrl = Url.Page( "/Account/ResetPassword", @@ -242,35 +245,37 @@ public async Task GenerateResetUrl(string userID) [HttpPut("Name")] [ServiceFilter(typeof(ApiAuthorizationFilter))] - public IActionResult Name([FromBody] string organizationName) + public async Task Name([FromBody] string organizationName) { - if (User.Identity.IsAuthenticated && - !DataService.GetUserByNameWithOrg(User.Identity.Name).IsAdministrator) + if (!Request.Headers.TryGetOrganizationId(out var orgId)) { return Unauthorized(); } + if (organizationName.Length > 25) { return BadRequest(); } - Request.Headers.TryGetValue("OrganizationID", out var orgID); - DataService.UpdateOrganizationName(orgID, organizationName.Trim()); + var result = await _dataService.UpdateOrganizationName(orgId, organizationName.Trim()); + _logger.LogResult(result); + if (!result.IsSuccess) + { + return BadRequest(result.Reason); + } return NoContent(); } [HttpPut("SetDefault")] [ServiceFilter(typeof(ApiAuthorizationFilter))] - public IActionResult SetDefault([FromBody] bool isDefault) + public async Task SetDefault([FromBody] bool isDefault) { - if (User.Identity.IsAuthenticated && - !DataService.GetUserByNameWithOrg(User.Identity.Name).IsServerAdmin) + if (!Request.Headers.TryGetOrganizationId(out var orgId)) { return Unauthorized(); } - Request.Headers.TryGetValue("OrganizationID", out var orgID); - DataService.SetIsDefaultOrganization(orgID, isDefault); + await _dataService.SetIsDefaultOrganization(orgId, isDefault); return NoContent(); } @@ -278,44 +283,39 @@ public IActionResult SetDefault([FromBody] bool isDefault) [ServiceFilter(typeof(ApiAuthorizationFilter))] public async Task SendInvite([FromBody] InviteViewModel invite) { - if (User.Identity.IsAuthenticated && - !DataService.GetUserByNameWithOrg(User.Identity.Name).IsAdministrator) + if (!Request.Headers.TryGetOrganizationId(out var orgId)) { return Unauthorized(); } + if (!ModelState.IsValid) { return BadRequest(); } - if (!Request.Headers.TryGetOrganizationId(out var orgId)) - { - return BadRequest("Organization ID is required."); - } - - if (!DataService.DoesUserExist(invite.InvitedUser)) + if (!_dataService.DoesUserExist(invite.InvitedUser)) { - var result = await DataService.CreateUser(invite.InvitedUser, invite.IsAdmin, orgId); + var result = await _dataService.CreateUser(invite.InvitedUser, invite.IsAdmin, orgId); if (!result.IsSuccess) { return BadRequest("There was an issue creating the new account."); } - var user = await UserManager.FindByEmailAsync(invite.InvitedUser); + var user = await _userManager.FindByEmailAsync(invite.InvitedUser); if (user is null) { return BadRequest("User not found."); } - await UserManager.ConfirmEmailAsync(user, await UserManager.GenerateEmailConfirmationTokenAsync(user)); + await _userManager.ConfirmEmailAsync(user, await _userManager.GenerateEmailConfirmationTokenAsync(user)); return Ok(); } else { - var newInvite = await DataService.AddInvite(orgId, invite); + var newInvite = await _dataService.AddInvite(orgId, invite); if (!newInvite.IsSuccess) { @@ -323,7 +323,7 @@ public async Task SendInvite([FromBody] InviteViewModel invite) } var inviteURL = $"{Request.Scheme}://{Request.Host}/Invite?id={newInvite.Value.ID}"; - var emailResult = await EmailSender.SendEmailAsync(invite.InvitedUser, "Invitation to Organization in Remotely", + var emailResult = await _emailSender.SendEmailAsync(invite.InvitedUser, "Invitation to Organization in Remotely", $@"

Hello! diff --git a/Server/API/RemoteControlController.cs b/Server/API/RemoteControlController.cs index 96f70988c..342056092 100644 --- a/Server/API/RemoteControlController.cs +++ b/Server/API/RemoteControlController.cs @@ -17,6 +17,7 @@ using Immense.RemoteControl.Shared.Helpers; using Microsoft.Build.Framework; using Microsoft.Extensions.Logging; +using Remotely.Server.Extensions; // For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 @@ -62,8 +63,12 @@ public RemoteControlController( [ServiceFilter(typeof(ApiAuthorizationFilter))] public async Task Get(string deviceID) { - Request.Headers.TryGetValue("OrganizationID", out var orgID); - return await InitiateRemoteControl(deviceID, orgID); + if (!Request.Headers.TryGetOrganizationId(out var orgId)) + { + return Unauthorized(); + } + + return await InitiateRemoteControl(deviceID, orgId); } [HttpPost] @@ -74,11 +79,17 @@ public async Task Post([FromBody] RemoteControlRequest rcRequest) return NotFound(); } - var orgId = _dataService.GetUserByNameWithOrg(rcRequest.Email)?.OrganizationID; + var userResult = await _dataService.GetUserByNameWithOrg(rcRequest.Email); + if (!userResult.IsSuccess) + { + return NotFound(); + } + + var orgId = userResult.Value.OrganizationID; var result = await _signInManager.PasswordSignInAsync(rcRequest.Email, rcRequest.Password, false, true); if (result.Succeeded && - _dataService.DoesUserHaveAccessToDevice(rcRequest.DeviceID, _dataService.GetUserByNameWithOrg(rcRequest.Email))) + _dataService.DoesUserHaveAccessToDevice(rcRequest.DeviceID, userResult.Value)) { _logger.LogInformation("API login successful for {rcRequestEmail}.", rcRequest.Email); return await InitiateRemoteControl(rcRequest.DeviceID, orgId); @@ -110,12 +121,20 @@ private async Task InitiateRemoteControl(string deviceID, string return Unauthorized(); } - if (User.Identity.IsAuthenticated && - !_dataService.DoesUserHaveAccessToDevice(targetDevice.ID, _dataService.GetUserByNameWithOrg(User.Identity.Name))) + if (User.Identity?.IsAuthenticated == true) { - return Unauthorized(); - } + var userResult = await _dataService.GetUserByNameWithOrg($"{User.Identity.Name}"); + + if (!userResult.IsSuccess) + { + return Unauthorized(); + } + if (!_dataService.DoesUserHaveAccessToDevice(targetDevice.ID, userResult.Value)) + { + return Unauthorized(); + } + } var sessionCount = _remoteControlSessionCache.Sessions .OfType() diff --git a/Server/API/ScriptResultsController.cs b/Server/API/ScriptResultsController.cs index 44233daf7..917f78137 100644 --- a/Server/API/ScriptResultsController.cs +++ b/Server/API/ScriptResultsController.cs @@ -1,7 +1,10 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; using Remotely.Server.Auth; +using Remotely.Server.Extensions; using Remotely.Server.Services; using Remotely.Shared.Models; +using System; using System.Text; using System.Threading.Tasks; @@ -28,20 +31,26 @@ public ScriptResultsController(IDataService dataService, IEmailSenderEx emailSen [ServiceFilter(typeof(ApiAuthorizationFilter))] public ActionResult DownloadAll() { - Request.Headers.TryGetValue("OrganizationID", out var orgID); + if (!Request.Headers.TryGetOrganizationId(out var orgId)) + { + return Unauthorized(); + } - var commandResults = _dataService.GetAllCommandResults(orgID); + var commandResults = _dataService.GetAllCommandResults(orgId); var content = System.Text.Json.JsonSerializer.Serialize(commandResults); return File(Encoding.UTF8.GetBytes(content), "application/octet-stream", "ScriptHistory.json"); } [HttpGet("{scriptId}")] [ServiceFilter(typeof(ApiAuthorizationFilter))] - public FileResult DownloadResults(string scriptId) + public ActionResult DownloadResults(string scriptId) { - Request.Headers.TryGetValue("OrganizationID", out var orgID); + if (!Request.Headers.TryGetOrganizationId(out var orgId)) + { + return Unauthorized(); + } - var commandResult = _dataService.GetScriptResult(scriptId, orgID); + var commandResult = _dataService.GetScriptResult(scriptId, orgId); var content = System.Text.Json.JsonSerializer.Serialize(commandResult); return File(Encoding.UTF8.GetBytes(content), "application/octet-stream", "ScriptResults.json"); } @@ -49,40 +58,55 @@ public FileResult DownloadResults(string scriptId) [HttpPost] [ServiceFilter(typeof(ExpiringTokenFilter))] - public async Task Post([FromBody] ScriptResult result) + public async Task> Post([FromBody] ScriptResult result) { _dataService.AddOrUpdateScriptResult(result); + var errorOut = result.ErrorOutput ?? Array.Empty(); + if (result.HadErrors && result.SavedScriptId.HasValue) { - var savedScript = await _dataService.GetSavedScript(result.SavedScriptId.Value); + var savedScriptResult = await _dataService.GetSavedScript(result.SavedScriptId.Value); + if (!savedScriptResult.IsSuccess) + { + return NotFound(); + } + + var savedScript = savedScriptResult.Value; if (savedScript.GenerateAlertOnError) { await _dataService.AddAlert(result.DeviceID, result.OrganizationID, $"Alert triggered while running script {savedScript.Name}.", - string.Join("\n", result.ErrorOutput)); + string.Join("\n", errorOut)); } if (savedScript.SendEmailOnError) { - var device = _dataService.GetDevice(result.DeviceID); + var deviceResult = await _dataService.GetDevice( + result.DeviceID, + query => query.Include(x => x.DeviceGroup)); - if (!string.IsNullOrWhiteSpace(device.DeviceGroupID)) + if (!deviceResult.IsSuccess) { - device.DeviceGroup = await _dataService.GetDeviceGroup(device.DeviceGroupID); + return NotFound(); } - await _emailSender.SendEmailAsync(savedScript.SendErrorEmailTo, - "Script Run Alert", - $"An alert was triggered while running script {savedScript.Name} on device {device.DeviceName}.

" + - $"Device ID: {device.ID}
" + - $"Device Name: {device.DeviceName}
" + - $"Device Alias: {device.Alias}
" + - $"Device Group (if any): {device.DeviceGroup?.Name}
" + + var device = deviceResult.Value; - $"Error Output:

" + - $"{string.Join("

", result.ErrorOutput)}"); + if (!string.IsNullOrWhiteSpace(savedScript.SendErrorEmailTo)) + { + await _emailSender.SendEmailAsync(savedScript.SendErrorEmailTo, + "Script Run Alert", + $"An alert was triggered while running script {savedScript.Name} on device {device.DeviceName}.

" + + $"Device ID: {device.ID}
" + + $"Device Name: {device.DeviceName}
" + + $"Device Alias: {device.Alias}
" + + $"Device Group (if any): {device.DeviceGroup?.Name}
" + + + $"Error Output:

" + + $"{string.Join("

", errorOut)}"); + } } } diff --git a/Server/Auth/ApiAuthorizationFilter.cs b/Server/Auth/ApiAuthorizationFilter.cs index 9a3e63e7a..8e5916de1 100644 --- a/Server/Auth/ApiAuthorizationFilter.cs +++ b/Server/Auth/ApiAuthorizationFilter.cs @@ -1,72 +1,90 @@ -using Microsoft.AspNetCore.Mvc; +using Immense.RemoteControl.Shared.Extensions; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Build.Framework; +using Microsoft.Extensions.Logging; using Remotely.Server.Services; +using Remotely.Shared; using System; using System.Net; using System.Text; +using System.Threading.Tasks; namespace Remotely.Server.Auth; -public class ApiAuthorizationFilter : IAuthorizationFilter +public class ApiAuthorizationFilter : IAsyncAuthorizationFilter { private readonly IDataService _dataService; + private readonly ILogger _logger; - public ApiAuthorizationFilter(IDataService dataService) + public ApiAuthorizationFilter( + IDataService dataService, + ILogger logger) { _dataService = dataService; + _logger = logger; } - public void OnAuthorization(AuthorizationFilterContext context) + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) { - - if (context.HttpContext.User.Identity.IsAuthenticated) + try + { + await Authorize(context); + } + catch (Exception ex) { - var orgID = _dataService.GetUserByNameWithOrg(context.HttpContext.User.Identity.Name)?.OrganizationID; - context.HttpContext.Request.Headers["OrganizationID"] = orgID; - return; + _logger.LogError(ex, "Error while authorizing API key."); } + } - if (context.HttpContext.Request.Headers.TryGetValue("Authorization", out var result)) + private async Task Authorize(AuthorizationFilterContext context) + { + var http = context.HttpContext; + http.Request.Headers["OrganizationID"] = string.Empty; + + if (http.User.Identity?.IsAuthenticated == true) { + var userResult = await _dataService.GetUserByNameWithOrg($"{http.User.Identity.Name}"); + if (userResult.IsSuccess && userResult.Value.IsAdministrator) + { + http.Request.Headers["OrganizationID"] = userResult.Value.OrganizationID; + return; + } + } - var headerComponents = result.ToString().Split(" "); + if (http.Request.Headers.TryGetValue(AppConstants.ApiKeyHeaderName, out var apiHeaderValue)) + { + var headerComponents = apiHeaderValue.ToString().Split(":"); if (headerComponents.Length < 2) { - context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + http.Response.StatusCode = (int)HttpStatusCode.Forbidden; context.Result = new UnauthorizedResult(); return; }; - var tokenType = headerComponents[0].Trim(); - var encodedToken = headerComponents[1].Trim(); + var keyId = headerComponents[0].Trim(); + var secret = headerComponents[1].Trim(); - switch (tokenType) - { - case "Basic": - byte[] data = Convert.FromBase64String(encodedToken); - string decodedString = Encoding.UTF8.GetString(data); + var isValid = await _dataService.ValidateApiKey( + keyId, + secret, + http.Request.Path, + $"{http.Connection.RemoteIpAddress}"); - var authComponents = decodedString.ToString().Split(":"); - if (authComponents.Length < 2) - { - context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; - context.Result = new UnauthorizedResult(); - return; - }; + if (isValid) + { + var keyResult = await _dataService.GetApiKey(keyId); - var keyId = authComponents[0]?.Trim(); - var apiSecret = authComponents[1]?.Trim(); - if (_dataService.ValidateApiKey(keyId, apiSecret, context.HttpContext.Request.Path, context.HttpContext.Connection.RemoteIpAddress.ToString())) - { - var orgID = _dataService.GetApiKey(keyId)?.OrganizationID; - context.HttpContext.Request.Headers["OrganizationID"] = orgID; - return; - } - break; + if (keyResult.IsSuccess) + { + http.Request.Headers["OrganizationID"] = keyResult.Value.OrganizationID; + return; + } } - } + http.Response.StatusCode = (int)HttpStatusCode.Forbidden; context.Result = new UnauthorizedResult(); } } diff --git a/Server/Auth/ExpiringTokenFilter.cs b/Server/Auth/ExpiringTokenFilter.cs index 3664ca78d..140055385 100644 --- a/Server/Auth/ExpiringTokenFilter.cs +++ b/Server/Auth/ExpiringTokenFilter.cs @@ -1,24 +1,23 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; -using Remotely.Server.Models; using Remotely.Server.Services; +using Remotely.Shared; using Remotely.Shared.Utilities; using System; -using System.Collections.Generic; -using System.Linq; +using System.Net; using System.Threading.Tasks; namespace Remotely.Server.Auth; -public class ExpiringTokenFilter : ActionFilterAttribute, IAuthorizationFilter +public class ExpiringTokenFilter : ActionFilterAttribute, IAsyncAuthorizationFilter { private readonly IDataService _dataService; private readonly IExpiringTokenService _expiringTokenService; private readonly ILogger _logger; - public ExpiringTokenFilter(IExpiringTokenService expiringTokenService, + public ExpiringTokenFilter( + IExpiringTokenService expiringTokenService, IDataService dataService, ILogger logger) { @@ -27,41 +26,70 @@ public ExpiringTokenFilter(IExpiringTokenService expiringTokenService, _logger = logger; } - public void OnAuthorization(AuthorizationFilterContext context) + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) { - if (context.HttpContext.User.Identity.IsAuthenticated) + try { - return; + await Authorize(context); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while authorizing expiring token."); } + } - if (!context.HttpContext.Request.Headers.TryGetValue("Authorization", out var authorization)) + private async Task Authorize(AuthorizationFilterContext context) + { + var http = context.HttpContext; + + if (http.User.Identity?.IsAuthenticated == true) { - context.Result = new UnauthorizedResult(); return; } - if (authorization.ToString().Contains(":")) + if (http.Request.Headers.TryGetValue(AppConstants.ApiKeyHeaderName, out var apiHeaderValue)) { - var keyId = authorization.ToString().Split(":")[0]?.Trim(); - var apiSecret = authorization.ToString().Split(":")[1]?.Trim(); - - if (_dataService.ValidateApiKey(keyId, apiSecret, context.HttpContext.Request.Path, context.HttpContext.Connection.RemoteIpAddress.ToString())) + var headerComponents = apiHeaderValue.ToString().Split(":"); + if (headerComponents.Length < 2) { - var orgID = _dataService.GetApiKey(keyId)?.OrganizationID; - context.HttpContext.Request.Headers["OrganizationID"] = orgID; + http.Response.StatusCode = (int)HttpStatusCode.Forbidden; + context.Result = new UnauthorizedResult(); return; + }; + + var keyId = headerComponents[0].Trim(); + var secret = headerComponents[1].Trim(); + + var isValid = await _dataService.ValidateApiKey( + keyId, + secret, + http.Request.Path, + $"{http.Connection.RemoteIpAddress}"); + + if (isValid) + { + var keyResult = await _dataService.GetApiKey(keyId); + + if (keyResult.IsSuccess) + { + _logger.LogDebug("Expiring token authorized via API key. Key ID: {keyId}.", keyId); + http.Request.Headers["OrganizationID"] = keyResult.Value.OrganizationID; + return; + } } } - - if (_expiringTokenService.TryGetExpiration(authorization.ToString(), out var expiration) && - expiration > DateTimeOffset.Now) + if (http.Request.Headers.TryGetValue(AppConstants.ExpiringTokenHeaderName, out var expiringToken)) { - _logger.LogDebug("Expiring token authorized. Token: {token}. Expiration: {expiration}", authorization, expiration); - return; + if (_expiringTokenService.TryGetExpiration(expiringToken.ToString(), out var expiration) && + expiration > Time.Now) + { + _logger.LogDebug("Expiring token authorized. Token: {token}. Expiration: {expiration}", expiringToken, expiration); + return; + } } - _logger.LogDebug("Expiring token not authorized. Token: {token}.", authorization); + _logger.LogDebug("Expiring token not authorization failed."); context.Result = new UnauthorizedResult(); } } diff --git a/Server/Extensions/IHeaderDictionaryExtensions.cs b/Server/Extensions/IHeaderDictionaryExtensions.cs index 82184f35b..e1900627d 100644 --- a/Server/Extensions/IHeaderDictionaryExtensions.cs +++ b/Server/Extensions/IHeaderDictionaryExtensions.cs @@ -1,10 +1,18 @@ using Microsoft.AspNetCore.Http; using System.Diagnostics.CodeAnalysis; +using Remotely.Server.Auth; namespace Remotely.Server.Extensions; public static class IHeaderDictionaryExtensions { + /// + /// If true, ensures that the user was authorized via , + /// and is either an Administrator or is using a valid API access token. + /// + /// + /// + /// public static bool TryGetOrganizationId( this IHeaderDictionary headers, [NotNullWhen(true)] out string? organizationId) diff --git a/Server/Pages/ApiKeys.razor b/Server/Pages/ApiKeys.razor index ddb40a5cf..2938ebba2 100644 --- a/Server/Pages/ApiKeys.razor +++ b/Server/Pages/ApiKeys.razor @@ -93,7 +93,12 @@ else var secret = RandomGenerator.GenerateString(36); var secretHash = new PasswordHasher().HashPassword(string.Empty, secret); - await DataService.CreateApiToken(Username, _createKeyName, secretHash); + var result = await DataService.CreateApiToken(Username, _createKeyName, secretHash); + if (!result.IsSuccess) + { + ToastService.ShowToast2(result.Reason, ToastType.Error); + return; + } RefreshData(); _alertMessage = "Key created."; _newKeySecret = secret; @@ -121,9 +126,9 @@ else { _apiTokens.Clear(); _apiTokens.AddRange(DataService.GetAllApiTokens(User.Id)); - _createKeyName = null; - _alertMessage = null; - _newKeySecret = null; + _createKeyName = string.Empty; + _alertMessage = string.Empty; + _newKeySecret = string.Empty; } @@ -142,8 +147,8 @@ else { ModalService.ShowModal("Using API Keys", new[] { - "API keys should be added to the request header when making API calls. The key should be \"Authorization\", and value should be \"{key-id}:{key-secret}\". Note the colon in between.", - "Example: Authorization=e5da1c09-e851-4bd4-a8c1-532144b3f894:7uY6h5zBYm4+90pZVek4lD6ewbQ83nKcDpghBfG00hhZu6Ew" + "API keys should be added to the request header when making API calls. The key should be \"X-Api-Key\", and value should be \"{key-id}:{key-secret}\". Note the colon in between.", + "Example: X-Api-Key=e5da1c09-e851-4bd4-a8c1-532144b3f894:7uY6h5zBYm4+90pZVek4lD6ewbQ83nKcDpghBfG00hhZu6Ew" }); } } diff --git a/Server/Services/AuthService.cs b/Server/Services/AuthService.cs index 2cb38d515..a6171749c 100644 --- a/Server/Services/AuthService.cs +++ b/Server/Services/AuthService.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Components.Authorization; +using Immense.RemoteControl.Shared; +using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Identity; using Remotely.Shared.Models; using System; @@ -13,7 +14,7 @@ namespace Remotely.Server.Services; public interface IAuthService { Task IsAuthenticated(); - Task GetUser(); + Task> GetUser(); } public class AuthService : IAuthService @@ -35,15 +36,15 @@ public async Task IsAuthenticated() return principal?.User?.Identity?.IsAuthenticated ?? false; } - public async Task GetUser() + public async Task> GetUser() { var principal = await _authProvider.GetAuthenticationStateAsync(); if (principal?.User?.Identity?.IsAuthenticated == true) { - return await _dataService.GetUserAsync(principal.User.Identity.Name); + return await _dataService.GetUserAsync($"{principal.User.Identity.Name}"); } - return null; + return Result.Fail("Not authenticated."); } } diff --git a/Server/Services/DataService.cs b/Server/Services/DataService.cs index 237530de9..1efea121b 100644 --- a/Server/Services/DataService.cs +++ b/Server/Services/DataService.cs @@ -116,7 +116,9 @@ public interface IDataService Task> GetDefaultOrganization(); - Task> GetDevice(string deviceId); + Task> GetDevice( + string deviceId, + Action>? includesBuilder = null); Task> GetDevice(string orgId, string deviceId); @@ -1268,11 +1270,18 @@ public async Task> GetDevice(string orgId, string deviceId) return Result.Ok(device); } - public async Task> GetDevice(string deviceId) + public async Task> GetDevice( + string deviceId, + Action>? includesBuilder = null) { using var dbContext = _appDbFactory.GetContext(); - - var device = await dbContext.Devices.FirstOrDefaultAsync(x => x.ID == deviceId); + + var query = dbContext.Devices.AsQueryable(); + if (includesBuilder is not null) + { + includesBuilder(query); + } + var device = await query.FirstOrDefaultAsync(x => x.ID == deviceId); if (device is null) { @@ -1710,7 +1719,7 @@ public async Task> GetUserById(string userId) public async Task> GetUserByNameWithOrg(string userName) { - if (userName == null) + if (string.IsNullOrWhiteSpace(userName)) { return Result.Fail("Username cannot be empty."); } diff --git a/Server/Shared/NavMenu.razor b/Server/Shared/NavMenu.razor index e56e0b4bb..2f4f1e272 100644 --- a/Server/Shared/NavMenu.razor +++ b/Server/Shared/NavMenu.razor @@ -131,29 +131,44 @@ -@code { +@code { private bool collapseNavMenu = true; - private RemotelyUser _user; - private Organization _organization; + private RemotelyUser? _user; + private Organization? _organization; protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); - _user = await AuthService.GetUser(); + var userResult = await AuthService.GetUser(); + + if (!userResult.IsSuccess) + { + return; + } + + _user = userResult.Value; if (!string.IsNullOrWhiteSpace(_user?.OrganizationID)) { - _organization = DataService.GetOrganizationById(_user.OrganizationID); + var orgResult = await DataService.GetOrganizationById(_user.OrganizationID); + if (orgResult.IsSuccess) + { + _organization = orgResult.Value; + } } else { - _organization = await DataService.GetDefaultOrganization(); + var orgResult = await DataService.GetDefaultOrganization(); + if (orgResult.IsSuccess) + { + _organization = orgResult.Value; + } } } - private string NavMenuCssClass => collapseNavMenu ? "collapse" : null; + private string NavMenuCssClass => collapseNavMenu ? "collapse" : ""; private void ToggleNavMenu() diff --git a/Shared/AppConstants.cs b/Shared/AppConstants.cs index c2179fb37..d2ab8a695 100644 --- a/Shared/AppConstants.cs +++ b/Shared/AppConstants.cs @@ -13,6 +13,8 @@ public class AppConstants public const int EmbeddedDataBlockLength = 256; public const long MaxUploadFileSize = 100_000_000; public const double ScriptRunExpirationMinutes = 30; + public const string ApiKeyHeaderName = "X-Api-Key"; + public const string ExpiringTokenHeaderName = "X-Expiring-Token"; #pragma warning disable IDE0230 // Use UTF-8 string literal public static byte[] EmbeddedImmySignature { get; } = new byte[] { 73, 109, 109, 121, 66, 111, 116, 32, 114, 111, 99, 107, 115, 32, 116, 104, 101, 32, 115, 111, 99, 107, 115, 32, 117, 110, 116, 105, 108, 32, 116, 104, 101, 32, 101, 113, 117, 105, 110, 111, 120, 33 }; diff --git a/submodules/Immense.RemoteControl b/submodules/Immense.RemoteControl index d82d3572b..4de148bc4 160000 --- a/submodules/Immense.RemoteControl +++ b/submodules/Immense.RemoteControl @@ -1 +1 @@ -Subproject commit d82d3572b7ae9d1b5091a0b83b0af9ffef663119 +Subproject commit 4de148bc4f37bf625186155373dd176e6c492207 From 28079f887a3e8d9d973ed4bd1e26ee6d9a1491a0 Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Tue, 25 Jul 2023 11:34:35 -0700 Subject: [PATCH 04/29] Refactor API controllers. --- Agent/Services/Linux/UpdaterLinux.cs | 6 +- Agent/Services/MacOS/UpdaterMac.cs | 6 +- Agent/Services/Windows/UpdaterWin.cs | 10 +- Server/API/AgentUpdateController.cs | 64 ++--------- Server/API/BrandingController.cs | 41 +++++-- Server/API/ClientDownloadsController.cs | 14 +-- Server/API/DevicesController.cs | 106 +++++++++++------- Server/API/FileSharingController.cs | 33 ++++-- Server/API/HealthCheckController.cs | 4 +- Server/API/LoginController.cs | 12 +- .../API/OrganizationManagementController.cs | 6 +- Server/API/RemoteControlController.cs | 8 +- Server/API/SavedScriptsController.cs | 10 +- Server/API/ScriptResultsController.cs | 4 - Server/API/ScriptingController.cs | 36 +++--- Server/API/ServerLogsController.cs | 8 +- .../Attributes/ActionRateLimiterAttribute.cs | 31 ----- Server/Auth/ExpiringTokenFilter.cs | 10 ++ Server/Data/AppDb.cs | 3 +- Server/Pages/Downloads.razor | 2 +- Server/Pages/ManageOrganization.razor.cs | 6 +- Server/Program.cs | 30 ++++- Server/RateLimiting/PolicyNames.cs | 6 + Server/Services/AuthService.cs | 2 +- Server/Services/DataService.cs | 56 ++++----- Shared/Models/EmbeddedServerData.cs | 2 +- Shared/Models/SharedFile.cs | 2 +- 27 files changed, 261 insertions(+), 257 deletions(-) delete mode 100644 Server/Attributes/ActionRateLimiterAttribute.cs create mode 100644 Server/RateLimiting/PolicyNames.cs diff --git a/Agent/Services/Linux/UpdaterLinux.cs b/Agent/Services/Linux/UpdaterLinux.cs index 3404a1d2e..141c0e4cd 100644 --- a/Agent/Services/Linux/UpdaterLinux.cs +++ b/Agent/Services/Linux/UpdaterLinux.cs @@ -131,7 +131,6 @@ public async Task InstallLatestVersion() _logger.LogInformation("Service Updater: Downloading install package."); - var downloadId = Guid.NewGuid().ToString(); var zipPath = Path.Combine(Path.GetTempPath(), "RemotelyUpdate.zip"); var installerPath = Path.Combine(Path.GetTempPath(), "RemotelyUpdate.sh"); @@ -156,12 +155,9 @@ await _updateDownloader.DownloadFile( installerPath); await _updateDownloader.DownloadFile( - $"{serverUrl}/API/AgentUpdate/DownloadPackage/linux/{downloadId}", + $"{serverUrl}/API/AgentUpdate/DownloadPackage/linux", zipPath); - using var httpClient = _httpClientFactory.CreateClient(); - using var response = httpClient.GetAsync($"{serverUrl}/api/AgentUpdate/ClearDownload/{downloadId}"); - _logger.LogInformation("Launching installer to perform update."); Process.Start("sudo", $"chmod +x {installerPath}").WaitForExit(); diff --git a/Agent/Services/MacOS/UpdaterMac.cs b/Agent/Services/MacOS/UpdaterMac.cs index 13aa6db07..5e9641bf6 100644 --- a/Agent/Services/MacOS/UpdaterMac.cs +++ b/Agent/Services/MacOS/UpdaterMac.cs @@ -132,7 +132,6 @@ public async Task InstallLatestVersion() _logger.LogInformation("Service Updater: Downloading install package."); - var downloadId = Guid.NewGuid().ToString(); var zipPath = Path.Combine(Path.GetTempPath(), "RemotelyUpdate.zip"); var installerPath = Path.Combine(Path.GetTempPath(), "RemotelyUpdate.sh"); @@ -142,12 +141,9 @@ await _updateDownloader.DownloadFile( installerPath); await _updateDownloader.DownloadFile( - $"{serverUrl}/API/AgentUpdate/DownloadPackage/macos-{_achitecture}/{downloadId}", + $"{serverUrl}/API/AgentUpdate/DownloadPackage/macos-{_achitecture}", zipPath); - using var httpClient = _httpClientFactory.CreateClient(); - using var response = httpClient.GetAsync($"{serverUrl}/api/AgentUpdate/ClearDownload/{downloadId}"); - _logger.LogInformation("Launching installer to perform update."); Process.Start("sudo", $"chmod +x {installerPath}").WaitForExit(); diff --git a/Agent/Services/Windows/UpdaterWin.cs b/Agent/Services/Windows/UpdaterWin.cs index e23a0e82c..c6205a457 100644 --- a/Agent/Services/Windows/UpdaterWin.cs +++ b/Agent/Services/Windows/UpdaterWin.cs @@ -102,7 +102,7 @@ public async Task CheckForUpdates() await InstallLatestVersion(); } - catch (WebException ex) when ((ex.Response as HttpWebResponse).StatusCode == HttpStatusCode.NotModified) + catch (WebException ex) when (ex.Response is HttpWebResponse response && response.StatusCode == HttpStatusCode.NotModified) { _logger.LogInformation("Service Updater: Version is current."); return; @@ -128,7 +128,6 @@ public async Task InstallLatestVersion() _logger.LogInformation("Service Updater: Downloading install package."); - var downloadId = Guid.NewGuid().ToString(); var zipPath = Path.Combine(Path.GetTempPath(), "RemotelyUpdate.zip"); var installerPath = Path.Combine(Path.GetTempPath(), "Remotely_Installer.exe"); @@ -139,12 +138,9 @@ await _updateDownloader.DownloadFile( installerPath); await _updateDownloader.DownloadFile( - $"{serverUrl}/api/AgentUpdate/DownloadPackage/win-{platform}/{downloadId}", + $"{serverUrl}/api/AgentUpdate/DownloadPackage/win-{platform}", zipPath); - using var httpClient = _httpClientFactory.CreateClient(); - using var response = httpClient.GetAsync($"{serverUrl}/api/AgentUpdate/ClearDownload/{downloadId}"); - foreach (var proc in Process.GetProcessesByName("Remotely_Installer")) { proc.Kill(); @@ -170,7 +166,7 @@ await _updateDownloader.DownloadFile( } } - private async void UpdateTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) + private async void UpdateTimer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e) { await CheckForUpdates(); } diff --git a/Server/API/AgentUpdateController.cs b/Server/API/AgentUpdateController.cs index f3d336e40..215c1e462 100644 --- a/Server/API/AgentUpdateController.cs +++ b/Server/API/AgentUpdateController.cs @@ -1,19 +1,15 @@ -using Immense.RemoteControl.Server.Abstractions; -using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.SignalR; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Primitives; using Remotely.Server.Hubs; +using Remotely.Server.RateLimiting; using Remotely.Server.Services; -using Remotely.Shared.Enums; using System; using System.IO; using System.Linq; using System.Net; -using System.Text; -using System.Threading; using System.Threading.Tasks; namespace Remotely.Server.API; @@ -22,10 +18,6 @@ namespace Remotely.Server.API; [ApiController] public class AgentUpdateController : ControllerBase { - private static readonly MemoryCache _downloadingAgents = new(new MemoryCacheOptions() - { ExpirationScanFrequency = TimeSpan.FromSeconds(10) }); - - private readonly IHubContext _agentHubContext; private readonly ILogger _logger; private readonly IApplicationConfig _appConfig; @@ -45,61 +37,20 @@ public AgentUpdateController(IWebHostEnvironment hostingEnv, _logger = logger; } - [HttpGet("[action]/{downloadId}")] - public ActionResult ClearDownload(string downloadId) - { - _logger.LogDebug("Clearing download ID {downloadId}.", downloadId); - _downloadingAgents.Remove(downloadId); - return Ok(); - } - [HttpGet("[action]/{platform}/{downloadId}")] - public async Task DownloadPackage(string platform, string downloadId) + [HttpGet("[action]/{platform}")] + [EnableRateLimiting(PolicyNames.AgentUpdateDownloads)] + public async Task DownloadPackage(string platform) { try { - var remoteIp = Request?.HttpContext?.Connection?.RemoteIpAddress.ToString(); + var remoteIp = $"{Request?.HttpContext?.Connection?.RemoteIpAddress}"; if (await CheckForDeviceBan(remoteIp)) { return BadRequest(); } - var startWait = DateTimeOffset.Now; - - while (_downloadingAgents.Count >= _appConfig.MaxConcurrentUpdates) - { - await Task.Delay(new Random().Next(100, 10000)); - - // A get operation is necessary to evaluate item eviction. - _downloadingAgents.TryGetValue(string.Empty, out _); - } - - var entryExpirationTime = TimeSpan.FromMinutes(3); - var tokenExpirationTime = entryExpirationTime.Add(TimeSpan.FromSeconds(15)); - - var expirationToken = new CancellationChangeToken( - new CancellationTokenSource(tokenExpirationTime).Token); - - var cacheOptions = new MemoryCacheEntryOptions() - .SetAbsoluteExpiration(entryExpirationTime) - .AddExpirationToken(expirationToken); - - _downloadingAgents.Set(downloadId, string.Empty, cacheOptions); - - var waitTime = DateTimeOffset.Now - startWait; - _logger.LogDebug( - "Download started after wait time of {waitTime}. " + - "ID: {downloadId}. " + - "IP: {remoteIp}. " + - "Current Downloads: {_downloadingAgentsCount}. Max Allowed: {_appConfigMaxConcurrentUpdates}", - waitTime, - downloadId, - remoteIp, - _downloadingAgents.Count, - _appConfig.MaxConcurrentUpdates); - - string filePath; switch (platform.ToLower()) @@ -133,7 +84,6 @@ public async Task DownloadPackage(string platform, string download } catch (Exception ex) { - _downloadingAgents.Remove(downloadId); _logger.LogError(ex, "Error while downloading package."); return StatusCode((int)HttpStatusCode.InternalServerError); } diff --git a/Server/API/BrandingController.cs b/Server/API/BrandingController.cs index 115466d7b..d9c6c9614 100644 --- a/Server/API/BrandingController.cs +++ b/Server/API/BrandingController.cs @@ -1,5 +1,8 @@ -using Microsoft.AspNetCore.Http; +using Immense.RemoteControl.Shared.Extensions; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Build.Framework; +using Microsoft.Extensions.Logging; using Remotely.Server.Services; using Remotely.Shared.Models; using System; @@ -14,27 +17,49 @@ namespace Remotely.Server.API; public class BrandingController : ControllerBase { private readonly IDataService _dataService; + private readonly ILogger _logger; - public BrandingController(IDataService dataService) + public BrandingController( + IDataService dataService, + ILogger logger) { _dataService = dataService; + _logger = logger; } [HttpGet("{organizationId}")] - public async Task Get(string organizationId) + public async Task> Get(string organizationId) { - return await _dataService.GetBrandingInfo(organizationId); + var result = await _dataService.GetBrandingInfo(organizationId); + _logger.LogResult(result); + if (!result.IsSuccess) + { + return NotFound(); + } + return result.Value; } [HttpGet] public async Task GetDefault() { - var defaultOrg = await _dataService.GetDefaultOrganization(); - if (defaultOrg is null) + var orgResult = await _dataService.GetDefaultOrganization(); + _logger.LogResult(orgResult); + + if (!orgResult.IsSuccess) + { + return new(); + } + + var brandingResult = await _dataService.GetBrandingInfo(orgResult.Value.ID); + _logger.LogResult(brandingResult); + + if (!orgResult.IsSuccess || + brandingResult.Value is null) { - return new BrandingInfo(); + return new(); } - return await _dataService.GetBrandingInfo(defaultOrg.ID); + + return brandingResult.Value; } } diff --git a/Server/API/ClientDownloadsController.cs b/Server/API/ClientDownloadsController.cs index 8d7f580b9..332379c1f 100644 --- a/Server/API/ClientDownloadsController.cs +++ b/Server/API/ClientDownloadsController.cs @@ -1,22 +1,16 @@ -using MailKit.Search; -using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; -using Microsoft.Build.Framework; using Microsoft.Extensions.Logging; using Remotely.Server.Auth; using Remotely.Server.Extensions; using Remotely.Server.Services; -using Remotely.Shared; using Remotely.Shared.Models; using Remotely.Shared.Services; -using Remotely.Shared.Utilities; using System; using System.Collections.Generic; using System.IO; using System.Text; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -147,7 +141,7 @@ private async Task GetBashInstaller(string fileName, string organ return File(fileBytes, "application/octet-stream", fileName); } - private async Task GetDesktopFile(string filePath, string organizationId = null) + private async Task GetDesktopFile(string filePath, string? organizationId = null) { LogRequest(nameof(GetDesktopFile)); @@ -158,7 +152,7 @@ private async Task GetDesktopFile(string filePath, string organiz if (!result.IsSuccess) { - throw result.Exception; + throw result.Exception ?? new Exception(result.Reason); } return File(result.Value, "application/octet-stream", Path.GetFileName(filePath)); @@ -187,7 +181,7 @@ private async Task GetInstallFile(string organizationId, string p if (!result.IsSuccess) { - throw result.Exception; + throw result.Exception ?? new Exception(result.Reason); } return File(result.Value, "application/octet-stream", "Remotely_Installer.exe"); diff --git a/Server/API/DevicesController.cs b/Server/API/DevicesController.cs index f963bb06c..b70370807 100644 --- a/Server/API/DevicesController.cs +++ b/Server/API/DevicesController.cs @@ -1,5 +1,7 @@ -using Microsoft.AspNetCore.Http.Extensions; +using Immense.RemoteControl.Shared.Extensions; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; using Remotely.Server.Auth; using Remotely.Server.Extensions; using Remotely.Server.Services; @@ -8,20 +10,22 @@ using System.Collections.Generic; using System.Threading.Tasks; -// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 - namespace Remotely.Server.API; [ApiController] [Route("api/[controller]")] public class DevicesController : ControllerBase { + private readonly IDataService _dataService; + private readonly ILogger _logger; - public DevicesController(IDataService dataService) + public DevicesController( + IDataService dataService, + ILogger logger) { - DataService = dataService; + _dataService = dataService; + _logger = logger; } - private IDataService DataService { get; set; } [HttpGet] @@ -33,81 +37,103 @@ public IEnumerable Get() return Array.Empty(); } - if (User.Identity?.IsAuthenticated == true && - !string.IsNullOrWhiteSpace(User.Identity.Name)) + if (User.Identity?.IsAuthenticated == true) { - return DataService.GetDevicesForUser(User.Identity.Name); + return _dataService.GetDevicesForUser($"{User.Identity.Name}"); } // Authorized with API key. Return all. - return DataService.GetAllDevices(orgId); + return _dataService.GetAllDevices(orgId); } [ServiceFilter(typeof(ApiAuthorizationFilter))] [HttpGet("{id}")] - public ActionResult Get(string id) + public async Task> Get(string id) { if (!Request.Headers.TryGetOrganizationId(out var orgId)) { return Unauthorized(); } - var device = DataService.GetDevice(orgId, id); + if (User.Identity?.IsAuthenticated == true) + { + var userResult = await _dataService.GetUserByName($"{User.Identity.Name}"); + _logger.LogResult(userResult); + + if (!userResult.IsSuccess) + { + return Unauthorized(); + } + + if (!_dataService.DoesUserHaveAccessToDevice(id, userResult.Value)) + { + return Unauthorized(); + } + } - if (User.Identity?.IsAuthenticated == true && - !string.IsNullOrWhiteSpace(User.Identity.Name) && - !DataService.DoesUserHaveAccessToDevice(id, DataService.GetUserByNameWithOrg(User.Identity.Name))) + var deviceResult = await _dataService.GetDevice(orgId, id); + _logger.LogResult(deviceResult); + + if (!deviceResult.IsSuccess) { - return Unauthorized(); + return NotFound(); } - return device; + + return deviceResult.Value; } [HttpPut] [ServiceFilter(typeof(ApiAuthorizationFilter))] - public async Task Update( - [FromBody] DeviceSetupOptions deviceOptions, - [FromHeader] string organizationId) + public async Task Update([FromBody] DeviceSetupOptions deviceOptions) { - if (string.IsNullOrWhiteSpace(deviceOptions?.DeviceID) || - string.IsNullOrWhiteSpace(organizationId)) - { - return BadRequest("DeviceOptions and OrganizationId are required."); - } - - if (string.IsNullOrWhiteSpace(User.Identity?.Name)) + if (!Request.Headers.TryGetOrganizationId(out var orgId)) { return Unauthorized(); } - - var user = DataService.GetUserByNameWithOrg(User.Identity.Name); - if (user is null) + + if (string.IsNullOrWhiteSpace(deviceOptions?.DeviceID)) { - return Unauthorized(); + return BadRequest("DeviceId is required."); } - if (User.Identity?.IsAuthenticated == true && - !DataService.DoesUserHaveAccessToDevice(deviceOptions.DeviceID, user)) + + if (User.Identity?.IsAuthenticated == true) { - return Unauthorized(); + var userResult = await _dataService.GetUserByName($"{User.Identity.Name}"); + _logger.LogResult(userResult); + + if (!userResult.IsSuccess) + { + return Unauthorized(); + } + + if (!_dataService.DoesUserHaveAccessToDevice(deviceOptions.DeviceID, userResult.Value)) + { + return Unauthorized(); + } + } - var device = await DataService.UpdateDevice(deviceOptions, organizationId); - if (device is null) + var deviceResult = await _dataService.UpdateDevice(deviceOptions, orgId); + _logger.LogResult(deviceResult); + + if (!deviceResult.IsSuccess) { return BadRequest(); } - return Created(Request.GetDisplayUrl(), device); + return Created(Request.GetDisplayUrl(), deviceResult.Value); } [HttpPost] public async Task Create([FromBody] DeviceSetupOptions deviceOptions) { - var device = await DataService.CreateDevice(deviceOptions); - if (device is null) + var result = await _dataService.CreateDevice(deviceOptions); + _logger.LogResult(result); + + if (!result.IsSuccess) { return BadRequest("Device already exists. Use Put with authorization to update the device."); } - return Created(Request.GetDisplayUrl(), device); + return Created(Request.GetDisplayUrl(), result.Value); } } diff --git a/Server/API/FileSharingController.cs b/Server/API/FileSharingController.cs index 69edb5452..d99ada7fd 100644 --- a/Server/API/FileSharingController.cs +++ b/Server/API/FileSharingController.cs @@ -1,7 +1,9 @@ using Microsoft.AspNetCore.Mvc; using Remotely.Server.Auth; +using Remotely.Server.Extensions; using Remotely.Server.Services; using Remotely.Shared; +using Remotely.Shared.Models; using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -21,14 +23,18 @@ public FileSharingController(IDataService dataService) [HttpGet("{id}")] [ServiceFilter(typeof(ExpiringTokenFilter))] - public ActionResult Get(string id) + public async Task Get(string id) { - var sharedFile = _dataService.GetSharedFiled(id); - if (sharedFile != null) + var sharedFileResult = await _dataService.GetSharedFiled(id); + + if (!sharedFileResult.IsSuccess) { - return File(sharedFile.FileContents, sharedFile.ContentType, sharedFile.FileName); + return NotFound(); } - return NotFound(); + + var sharedFile = sharedFileResult.Value; + var contentType = sharedFile.ContentType ?? "application/octet-stream"; + return File(sharedFile.FileContents, contentType, sharedFile.FileName); } [HttpPost] @@ -36,18 +42,23 @@ public ActionResult Get(string id) [RequestSizeLimit(AppConstants.MaxUploadFileSize)] public async Task> Post() { - if (Request?.Form?.Files?.Count !> 0) + if (Request.Form.Files.Count !> 0) { return Array.Empty(); } - var fileIDs = new List(); + var fileIds = new List(); + + if (!Request.Headers.TryGetOrganizationId(out var orgId)) + { + orgId = string.Empty; + } + foreach (var file in Request.Form.Files) { - var orgID = User.Identity.IsAuthenticated ? _dataService.GetUserByNameWithOrg(User.Identity.Name).OrganizationID : null; - var id = await _dataService.AddSharedFile(file, orgID); - fileIDs.Add(id); + var id = await _dataService.AddSharedFile(file, orgId); + fileIds.Add(id); } - return fileIDs; + return fileIds; } } diff --git a/Server/API/HealthCheckController.cs b/Server/API/HealthCheckController.cs index 331c9e992..7caa28673 100644 --- a/Server/API/HealthCheckController.cs +++ b/Server/API/HealthCheckController.cs @@ -26,7 +26,7 @@ public HealthCheckController(IDataService dataService) [HttpGet] public async Task Get() { - var orgCount = await _dataService.GetOrganizationCountAsync(); - return Ok($"Organization Count: {orgCount}"); + _ = await _dataService.GetOrganizationCountAsync(); + return NoContent(); } } diff --git a/Server/API/LoginController.cs b/Server/API/LoginController.cs index cb9b4ea17..ddbee3b5f 100644 --- a/Server/API/LoginController.cs +++ b/Server/API/LoginController.cs @@ -49,11 +49,15 @@ public LoginController( [HttpGet("Logout")] public async Task Logout() { - string orgId = null; - if (HttpContext?.User?.Identity?.IsAuthenticated == true) { - orgId = _dataService.GetUserByNameWithOrg(HttpContext.User.Identity.Name)?.OrganizationID; + var userResult = await _dataService.GetUserByName($"{HttpContext.User.Identity.Name}"); + + if (!userResult.IsSuccess) + { + return NotFound(); + } + var activeSessions = _remoteControlSessionCache .Sessions .Where(x => x.RequesterUserName == HttpContext.User.Identity.Name); @@ -77,8 +81,6 @@ public async Task Post([FromBody] ApiLogin login) return NotFound(); } - var orgId = _dataService.GetUserByNameWithOrg(login.Email)?.OrganizationID; - var result = await _signInManager.PasswordSignInAsync(login.Email, login.Password, false, true); if (result.Succeeded) { diff --git a/Server/API/OrganizationManagementController.cs b/Server/API/OrganizationManagementController.cs index 84d7562ee..5381190ef 100644 --- a/Server/API/OrganizationManagementController.cs +++ b/Server/API/OrganizationManagementController.cs @@ -54,14 +54,14 @@ public async Task ChangeIsAdmin(string userId, [FromBody] bool is if (User.Identity?.IsAuthenticated == true) { - var userResult = await _dataService.GetUserByNameWithOrg($"{User.Identity.Name}"); + var userResult = await _dataService.GetUserByName($"{User.Identity.Name}"); if (userResult.IsSuccess && userResult.Value.Id == userId) { return BadRequest("You can't remove administrator rights from yourself."); } } - _dataService.ChangeUserIsAdmin(orgId, userId, isAdmin); + await _dataService.ChangeUserIsAdmin(orgId, userId, isAdmin); return NoContent(); } @@ -96,7 +96,7 @@ public async Task DeleteUser(string userId) if (User.Identity?.IsAuthenticated == true) { - var userResult = await _dataService.GetUserByNameWithOrg($"{User.Identity.Name}"); + var userResult = await _dataService.GetUserByName($"{User.Identity.Name}"); if (userResult.IsSuccess && userResult.Value.Id == userId) { return BadRequest("You can't delete yourself here. You must go to the Personal Data page to delete your own account."); diff --git a/Server/API/RemoteControlController.cs b/Server/API/RemoteControlController.cs index 342056092..a20be9cff 100644 --- a/Server/API/RemoteControlController.cs +++ b/Server/API/RemoteControlController.cs @@ -1,21 +1,17 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; -using Remotely.Server.Attributes; using Remotely.Server.Hubs; using Remotely.Server.Models; using Remotely.Server.Services; -using Remotely.Shared.Utilities; using Remotely.Shared.Models; using System; using System.Linq; using System.Threading.Tasks; using Remotely.Server.Auth; using Immense.RemoteControl.Server.Services; -using Remotely.Server.Services.RcImplementations; using Immense.RemoteControl.Server.Abstractions; using Immense.RemoteControl.Shared.Helpers; -using Microsoft.Build.Framework; using Microsoft.Extensions.Logging; using Remotely.Server.Extensions; @@ -79,7 +75,7 @@ public async Task Post([FromBody] RemoteControlRequest rcRequest) return NotFound(); } - var userResult = await _dataService.GetUserByNameWithOrg(rcRequest.Email); + var userResult = await _dataService.GetUserByName(rcRequest.Email); if (!userResult.IsSuccess) { return NotFound(); @@ -123,7 +119,7 @@ private async Task InitiateRemoteControl(string deviceID, string if (User.Identity?.IsAuthenticated == true) { - var userResult = await _dataService.GetUserByNameWithOrg($"{User.Identity.Name}"); + var userResult = await _dataService.GetUserByName($"{User.Identity.Name}"); if (!userResult.IsSuccess) { diff --git a/Server/API/SavedScriptsController.cs b/Server/API/SavedScriptsController.cs index 9e8617087..86c1aebcc 100644 --- a/Server/API/SavedScriptsController.cs +++ b/Server/API/SavedScriptsController.cs @@ -23,8 +23,14 @@ public SavedScriptsController(IDataService dataService) [ServiceFilter(typeof(ExpiringTokenFilter))] [HttpGet("{scriptId}")] - public async Task GetScript(Guid scriptId) + public async Task> GetScript(Guid scriptId) { - return await _dataService.GetSavedScript(scriptId); + var result = await _dataService.GetSavedScript(scriptId); + if (!result.IsSuccess) + { + return NotFound(); + } + + return result.Value; } } diff --git a/Server/API/ScriptResultsController.cs b/Server/API/ScriptResultsController.cs index 917f78137..b47a5eb7b 100644 --- a/Server/API/ScriptResultsController.cs +++ b/Server/API/ScriptResultsController.cs @@ -8,8 +8,6 @@ using System.Text; using System.Threading.Tasks; -// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 - namespace Remotely.Server.API; [Route("api/[controller]")] @@ -25,8 +23,6 @@ public ScriptResultsController(IDataService dataService, IEmailSenderEx emailSen _emailSender = emailSenderEx; } - - // GET: api/ [HttpGet] [ServiceFilter(typeof(ApiAuthorizationFilter))] public ActionResult DownloadAll() diff --git a/Server/API/ScriptingController.cs b/Server/API/ScriptingController.cs index caa4bb491..d7d607f9e 100644 --- a/Server/API/ScriptingController.cs +++ b/Server/API/ScriptingController.cs @@ -6,15 +6,13 @@ using Remotely.Shared.Utilities; using Remotely.Shared.Models; using System; -using System.Collections.Generic; using System.IO; -using System.Linq; using System.Threading.Tasks; using Remotely.Shared.Enums; using Remotely.Server.Auth; -using Immense.RemoteControl.Server.Abstractions; using Immense.RemoteControl.Shared.Helpers; using Remotely.Shared; +using Remotely.Server.Extensions; namespace Remotely.Server.API; @@ -47,6 +45,11 @@ public ScriptingController(UserManager userManager, [HttpPost("[action]/{mode}/{deviceID}")] public async Task> ExecuteCommand(string mode, string deviceID) { + if (!Request.Headers.TryGetOrganizationId(out var orgId)) + { + return Unauthorized(); + } + if (!Enum.TryParse(mode, true, out var shell)) { return BadRequest("Unable to parse shell type. Use either PSCore, WinPS, Bash, or CMD."); @@ -59,20 +62,22 @@ public async Task> ExecuteCommand(string mode, string } var userID = string.Empty; - if (Request.HttpContext.User.Identity.IsAuthenticated) + if (Request.HttpContext.User.Identity?.IsAuthenticated == true) { var username = Request.HttpContext.User.Identity.Name; - var user = await _userManager.FindByNameAsync(username); - userID = user.Id; - if (!_dataService.DoesUserHaveAccessToDevice(deviceID, user)) + var userResult = await _dataService.GetUserByName($"{username}"); + + if (!userResult.IsSuccess) { return Unauthorized(); } + if (!_dataService.DoesUserHaveAccessToDevice(deviceID, userResult.Value)) + { + return Unauthorized(); + } } - Request.Headers.TryGetValue("OrganizationID", out var orgID); - if (!_serviceSessionCache.TryGetByDeviceId(deviceID, out var device)) { return NotFound(); @@ -83,7 +88,7 @@ public async Task> ExecuteCommand(string mode, string return NotFound(); } - if (device.OrganizationID != orgID) + if (device.OrganizationID != orgId) { return Unauthorized(); } @@ -99,9 +104,14 @@ public async Task> ExecuteCommand(string mode, string { return NotFound(); } - AgentHub.ApiScriptResults.TryGetValue(requestID, out var commandID); + AgentHub.ApiScriptResults.TryGetValue(requestID, out var commandId); AgentHub.ApiScriptResults.Remove(requestID); - var result = _dataService.GetScriptResult(commandID.ToString(), orgID); - return result; + + var scriptResult = await _dataService.GetScriptResult($"{commandId}", orgId); + if (!scriptResult.IsSuccess) + { + return NotFound(); + } + return scriptResult.Value; } } diff --git a/Server/API/ServerLogsController.cs b/Server/API/ServerLogsController.cs index 85e9498bf..d094135b2 100644 --- a/Server/API/ServerLogsController.cs +++ b/Server/API/ServerLogsController.cs @@ -34,9 +34,15 @@ public async Task Download() HttpContext.Connection.RemoteIpAddress); var zipFile = await _logsManager.ZipAllLogs(); + Response.OnCompleted(() => { - Directory.Delete(zipFile.DirectoryName, true); + if (zipFile.Directory is null) + { + return Task.CompletedTask; + } + + zipFile.Directory.Delete(true); return Task.CompletedTask; }); diff --git a/Server/Attributes/ActionRateLimiterAttribute.cs b/Server/Attributes/ActionRateLimiterAttribute.cs deleted file mode 100644 index 58872b57e..000000000 --- a/Server/Attributes/ActionRateLimiterAttribute.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Extensions.Caching.Memory; -using System; -using System.Net; - -namespace Remotely.Server.Attributes; - -[AttributeUsage(AttributeTargets.Method)] -public class ActionRateLimiterAttribute : ActionFilterAttribute -{ - public string Action { get; set; } - public int TimeoutInSeconds { get; set; } = 5; - private static MemoryCache RequestCache { get; } = new MemoryCache(new MemoryCacheOptions()); - - - public override void OnActionExecuting(ActionExecutingContext context) - { - var ip = context.HttpContext.Request.HttpContext.Connection.RemoteIpAddress; - var key = $"Action-{ip}"; - - if (!RequestCache.TryGetValue(key, out _)) - { - RequestCache.Set(key, true, TimeSpan.FromSeconds(TimeoutInSeconds)); - } - else - { - context.HttpContext.Response.StatusCode = (int)HttpStatusCode.TooManyRequests; - } - base.OnActionExecuting(context); - } -} diff --git a/Server/Auth/ExpiringTokenFilter.cs b/Server/Auth/ExpiringTokenFilter.cs index 140055385..046279577 100644 --- a/Server/Auth/ExpiringTokenFilter.cs +++ b/Server/Auth/ExpiringTokenFilter.cs @@ -41,9 +41,19 @@ public async Task OnAuthorizationAsync(AuthorizationFilterContext context) private async Task Authorize(AuthorizationFilterContext context) { var http = context.HttpContext; + http.Request.Headers["OrganizationID"] = string.Empty; if (http.User.Identity?.IsAuthenticated == true) { + var userResult = await _dataService.GetUserByName($"{http.User.Identity.Name}"); + if (!userResult.IsSuccess) + { + http.Response.StatusCode = (int)HttpStatusCode.Forbidden; + context.Result = new UnauthorizedResult(); + return; + } + + http.Request.Headers["OrganizationID"] = userResult.Value.OrganizationID; return; } diff --git a/Server/Data/AppDb.cs b/Server/Data/AppDb.cs index 73f66437f..6e9d07dd0 100644 --- a/Server/Data/AppDb.cs +++ b/Server/Data/AppDb.cs @@ -66,7 +66,8 @@ protected override void OnModelCreating(ModelBuilder builder) .WithOne(x => x.Organization); builder.Entity() .HasMany(x => x.SharedFiles) - .WithOne(x => x.Organization); + .WithOne(x => x.Organization) + .IsRequired(false); builder.Entity() .HasMany(x => x.ApiTokens) .WithOne(x => x.Organization); diff --git a/Server/Pages/Downloads.razor b/Server/Pages/Downloads.razor index fdf0b9153..51cebfd09 100644 --- a/Server/Pages/Downloads.razor +++ b/Server/Pages/Downloads.razor @@ -186,7 +186,7 @@ if (_isAuthenticated) { - var currentUser = await DataService.GetUserAsync(authState.User.Identity.Name); + var currentUser = await DataService.GetUserByName(authState.User.Identity.Name); _organizationId = currentUser.OrganizationID; } else diff --git a/Server/Pages/ManageOrganization.razor.cs b/Server/Pages/ManageOrganization.razor.cs index cfe604bcc..c43efbc26 100644 --- a/Server/Pages/ManageOrganization.razor.cs +++ b/Server/Pages/ManageOrganization.razor.cs @@ -279,7 +279,7 @@ private async Task SendInvite() var result = await DataService.CreateUser(_inviteEmail, _inviteAsAdmin, User.OrganizationID); if (result) { - var user = await DataService.GetUserAsync(_inviteEmail); + var user = await DataService.GetUserByName(_inviteEmail); await UserManager.ConfirmEmailAsync(user, await UserManager.GenerateEmailConfirmationTokenAsync(user)); @@ -337,7 +337,7 @@ private async Task SendInvite() } } - private void SetUserIsAdmin(ChangeEventArgs args, RemotelyUser orgUser) + private async Task SetUserIsAdmin(ChangeEventArgs args, RemotelyUser orgUser) { if (!User.IsAdministrator) { @@ -349,7 +349,7 @@ private void SetUserIsAdmin(ChangeEventArgs args, RemotelyUser orgUser) return; } - DataService.ChangeUserIsAdmin(User.OrganizationID, orgUser.Id, isAdmin); + await DataService.ChangeUserIsAdmin(User.OrganizationID, orgUser.Id, isAdmin); ToastService.ShowToast("Administrator value set."); } diff --git a/Server/Program.cs b/Server/Program.cs index ae20299e0..ec6b1b78c 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -37,6 +37,8 @@ using Immense.RemoteControl.Server.Services; using Serilog; using Nihs.SimpleMessenger; +using Microsoft.AspNetCore.RateLimiting; +using RatePolicyNames = Remotely.Server.RateLimiting.PolicyNames; var builder = WebApplication.CreateBuilder(args); var configuration = builder.Configuration; @@ -53,7 +55,12 @@ builder.Logging.AddEventLog(); } -var dbProvider = configuration["ApplicationOptions:DBProvider"].ToLower(); +var dbProvider = configuration["ApplicationOptions:DBProvider"]?.ToLower(); +if (string.IsNullOrWhiteSpace(dbProvider)) +{ + throw new InvalidOperationException("DBProvider is missing from appsettings.json."); +} + if (dbProvider == "sqlite") { services.AddDbContext(options => @@ -191,7 +198,21 @@ c.SwaggerDoc("v1", new OpenApiInfo { Title = "Remotely API", Version = "v1" }); }); +services.AddRateLimiter(options => +{ + options.AddConcurrencyLimiter(RatePolicyNames.AgentUpdateDownloads, clOptions => + { + clOptions.QueueLimit = int.MaxValue; + + var concurrentPermits = configuration.GetSection("ApplicationOptions:MaxConcurrentUpdates").Get(); + if (concurrentPermits <= 0) + { + concurrentPermits = 10; + } + clOptions.PermitLimit = concurrentPermits; + }); +}); services.AddHttpClient(); services.AddLogging(); services.AddScoped(); @@ -233,6 +254,9 @@ services.AddSingleton(); var app = builder.Build(); + +app.UseRateLimiter(); + var appConfig = app.Services.GetRequiredService(); if (appConfig.UseHttpLogging) @@ -250,11 +274,11 @@ else { app.UseExceptionHandler("/Error"); - if (bool.Parse(app.Configuration["ApplicationOptions:UseHsts"])) + if (bool.TryParse(app.Configuration["ApplicationOptions:UseHsts"], out var hsts) && hsts) { app.UseHsts(); } - if (bool.Parse(app.Configuration["ApplicationOptions:RedirectToHttps"])) + if (bool.TryParse(app.Configuration["ApplicationOptions:RedirectToHttps"], out var redirect) && redirect) { app.UseHttpsRedirection(); } diff --git a/Server/RateLimiting/PolicyNames.cs b/Server/RateLimiting/PolicyNames.cs new file mode 100644 index 000000000..f8aa94aae --- /dev/null +++ b/Server/RateLimiting/PolicyNames.cs @@ -0,0 +1,6 @@ +namespace Remotely.Server.RateLimiting; + +public static class PolicyNames +{ + public const string AgentUpdateDownloads = nameof(AgentUpdateDownloads); +} diff --git a/Server/Services/AuthService.cs b/Server/Services/AuthService.cs index a6171749c..7cc451d02 100644 --- a/Server/Services/AuthService.cs +++ b/Server/Services/AuthService.cs @@ -42,7 +42,7 @@ public async Task> GetUser() if (principal?.User?.Identity?.IsAuthenticated == true) { - return await _dataService.GetUserAsync($"{principal.User.Identity.Name}"); + return await _dataService.GetUserByName($"{principal.User.Identity.Name}"); } return Result.Fail("Not authenticated."); diff --git a/Server/Services/DataService.cs b/Server/Services/DataService.cs index 1efea121b..cfafa9988 100644 --- a/Server/Services/DataService.cs +++ b/Server/Services/DataService.cs @@ -50,7 +50,7 @@ public interface IDataService bool AddUserToDeviceGroup(string orgId, string groupId, string userName, out string resultMessage); - void ChangeUserIsAdmin(string organizationId, string targetUserId, bool isAdmin); + Task ChangeUserIsAdmin(string organizationId, string targetUserId, bool isAdmin); Task CleanupOldRecords(); @@ -174,11 +174,11 @@ Task> GetDeviceGroup( int GetTotalDevices(); - Task> GetUserAsync(string username); - Task> GetUserById(string userId); - Task> GetUserByNameWithOrg(string userName); + Task> GetUserByName( + string userName, + Action>? includesBuilder = null); Task> GetUserOptions(string userName); @@ -562,7 +562,7 @@ public async Task AddSharedFile(IBrowserFile file, string organizationId progressCallback.Invoke(1, file.Name); - return await AddSharedFileInternal(file.Name, fileContents, file.ContentType, organizationId); + return await AddSharedFileImpl(file.Name, fileContents, file.ContentType, organizationId); } public async Task AddSharedFile(IFormFile file, string organizationId) @@ -571,7 +571,7 @@ public async Task AddSharedFile(IFormFile file, string organizationId) using var stream = file.OpenReadStream(); await stream.ReadAsync(fileContents.AsMemory(0, (int)file.Length)); - return await AddSharedFileInternal(file.Name, fileContents, file.ContentType, organizationId); + return await AddSharedFileImpl(file.Name, fileContents, file.ContentType, organizationId); } public bool AddUserToDeviceGroup(string orgId, string groupId, string userName, out string resultMessage) @@ -622,11 +622,11 @@ public bool AddUserToDeviceGroup(string orgId, string groupId, string userName, return true; } - public void ChangeUserIsAdmin(string organizationId, string targetUserId, bool isAdmin) + public async Task ChangeUserIsAdmin(string organizationId, string targetUserId, bool isAdmin) { using var dbContext = _appDbFactory.GetContext(); - var targetUser = dbContext.Users.FirstOrDefault(x => + var targetUser = await dbContext.Users.FirstOrDefaultAsync(x => x.OrganizationID == organizationId && x.Id == targetUserId); @@ -795,7 +795,7 @@ public async Task DeleteAllAlerts(string orgId, string? userName = null) if (!string.IsNullOrWhiteSpace(userName)) { - var userResult = await GetUserByNameWithOrg(userName); + var userResult = await GetUserByName(userName); if (userResult.IsSuccess) { @@ -1277,10 +1277,7 @@ public async Task> GetDevice( using var dbContext = _appDbFactory.GetContext(); var query = dbContext.Devices.AsQueryable(); - if (includesBuilder is not null) - { - includesBuilder(query); - } + includesBuilder?.Invoke(query); var device = await query.FirstOrDefaultAsync(x => x.ID == deviceId); if (device is null) @@ -1683,23 +1680,6 @@ public int GetTotalDevices() return dbContext.Devices.Count(); } - public async Task> GetUserAsync(string username) - { - if (string.IsNullOrWhiteSpace(username)) - { - return Result.Fail("Username cannot be empty."); - } - using var dbContext = _appDbFactory.GetContext(); - - var user = await dbContext.Users.FirstOrDefaultAsync(x => x.UserName == username); - - if (user is null) - { - return Result.Fail("User not found."); - } - return Result.Ok(user); - } - public async Task> GetUserById(string userId) { if (string.IsNullOrWhiteSpace(userId)) @@ -1717,7 +1697,9 @@ public async Task> GetUserById(string userId) return Result.Ok(user); } - public async Task> GetUserByNameWithOrg(string userName) + public async Task> GetUserByName( + string userName, + Action>? includesBuilder = null) { if (string.IsNullOrWhiteSpace(userName)) { @@ -1726,9 +1708,11 @@ public async Task> GetUserByNameWithOrg(string userName) using var dbContext = _appDbFactory.GetContext(); - var user = await dbContext.Users - .Include(x => x.Organization) - .FirstOrDefaultAsync(x => x.UserName!.ToLower().Trim() == userName.ToLower().Trim()); + var query = dbContext.Users.AsQueryable(); + includesBuilder?.Invoke(query); + + var user = await query.FirstOrDefaultAsync(x => + x.UserName!.ToLower().Trim() == userName.ToLower().Trim()); if (user is null) { @@ -1966,7 +1950,7 @@ public async Task TempPasswordSignIn(string email, string password) return false; } - var userResult = await GetUserByNameWithOrg(email); + var userResult = await GetUserByName(email); if (!userResult.IsSuccess) { @@ -2152,7 +2136,7 @@ public async Task ValidateApiKey(string keyId, string apiSecret, string re return isValid; } - private async Task AddSharedFileInternal( + private async Task AddSharedFileImpl( string fileName, byte[] fileContents, string contentType, diff --git a/Shared/Models/EmbeddedServerData.cs b/Shared/Models/EmbeddedServerData.cs index bd16e1ada..e170cac4c 100644 --- a/Shared/Models/EmbeddedServerData.cs +++ b/Shared/Models/EmbeddedServerData.cs @@ -14,7 +14,7 @@ public class EmbeddedServerData { [SerializationConstructor] [JsonConstructor] - public EmbeddedServerData(Uri serverUrl, string organizationId) + public EmbeddedServerData(Uri serverUrl, string? organizationId) { ServerUrl = serverUrl; OrganizationId = organizationId ?? string.Empty; diff --git a/Shared/Models/SharedFile.cs b/Shared/Models/SharedFile.cs index 7f26502e5..72fed5b84 100644 --- a/Shared/Models/SharedFile.cs +++ b/Shared/Models/SharedFile.cs @@ -14,5 +14,5 @@ public class SharedFile public byte[] FileContents { get; set; } = Array.Empty(); public DateTimeOffset Timestamp { get; set; } = DateTimeOffset.Now; public Organization? Organization { get; set; } - public string OrganizationID { get; set; } = null!; + public string? OrganizationID { get; set; } } From 0f9ea4957b06ddf6b2aa5f05c65a755faaaa9a5d Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Tue, 25 Jul 2023 14:05:59 -0700 Subject: [PATCH 05/29] Clean up errors and majority of warnings. --- Agent.Installer.Win/Utilities/Logger.cs | 2 +- Server/Auth/ApiAuthorizationFilter.cs | 2 +- Server/Components/AuthComponentBase.cs | 27 ++++- Server/Components/Devices/DeviceCard.razor.cs | 2 +- .../Components/Devices/DevicesFrame.razor.cs | 59 +++++----- Server/Components/Devices/Terminal.razor.cs | 19 +++- Server/Components/Scripts/RunScript.razor.cs | 40 ++++--- .../Components/Scripts/SavedScripts.razor.cs | 34 +++--- .../Components/Scripts/ScriptSchedules.razor | 6 +- .../Scripts/ScriptSchedules.razor.cs | 68 ++++++++---- Server/Components/Scripts/ScriptTreeNode.cs | 4 +- Server/Data/AppDb.cs | 8 +- Server/Hubs/AgentHub.cs | 11 +- Server/Hubs/CircuitConnection.cs | 105 ++++++++++++------ Server/Pages/ApiKeys.razor | 6 +- Server/Pages/Branding.razor | 18 ++- Server/Pages/DeviceDetails.razor | 48 ++++---- Server/Pages/DeviceDetails.razor.cs | 89 ++++++++++----- Server/Pages/Downloads.razor | 24 ++-- Server/Pages/GetSupport.cshtml.cs | 25 +++-- Server/Pages/Invite.cshtml | 7 +- Server/Pages/Invite.cshtml.cs | 16 ++- Server/Pages/ManageOrganization.razor.cs | 25 ++++- Server/Pages/Shared/_Layout.cshtml | 15 ++- Server/Pages/_Host.cshtml | 5 +- Server/Services/CircuitManager.cs | 13 ++- Server/Services/ClientAppState.cs | 8 +- Server/Services/DataService.cs | 23 +++- .../ViewerPageDataProvider.cs | 13 ++- Shared/Models/Device.cs | 2 +- Tests/Server.Tests/DataServiceTests.cs | 72 ++++++------ Tests/Server.Tests/TestData.cs | 48 ++++---- 32 files changed, 541 insertions(+), 303 deletions(-) diff --git a/Agent.Installer.Win/Utilities/Logger.cs b/Agent.Installer.Win/Utilities/Logger.cs index f1e16ce61..e5320dd12 100644 --- a/Agent.Installer.Win/Utilities/Logger.cs +++ b/Agent.Installer.Win/Utilities/Logger.cs @@ -52,7 +52,7 @@ public static void Write(Exception ex) while (exception != null) { - File.AppendAllText(LogsPath, $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}\t[Error]\t{exception?.Message}\t{exception?.StackTrace}\t{exception?.Source}{Environment.NewLine}"); + File.AppendAllText(LogsPath, $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}\t[Error]\t{exception.Message}\t{exception.StackTrace}\t{exception.Source}{Environment.NewLine}"); Console.WriteLine(exception.Message); exception = exception.InnerException; } diff --git a/Server/Auth/ApiAuthorizationFilter.cs b/Server/Auth/ApiAuthorizationFilter.cs index 8e5916de1..3eb306c16 100644 --- a/Server/Auth/ApiAuthorizationFilter.cs +++ b/Server/Auth/ApiAuthorizationFilter.cs @@ -45,7 +45,7 @@ private async Task Authorize(AuthorizationFilterContext context) if (http.User.Identity?.IsAuthenticated == true) { - var userResult = await _dataService.GetUserByNameWithOrg($"{http.User.Identity.Name}"); + var userResult = await _dataService.GetUserByName($"{http.User.Identity.Name}"); if (userResult.IsSuccess && userResult.Value.IsAdministrator) { http.Request.Headers["OrganizationID"] = userResult.Value.OrganizationID; diff --git a/Server/Components/AuthComponentBase.cs b/Server/Components/AuthComponentBase.cs index c8c1fda81..5666ed963 100644 --- a/Server/Components/AuthComponentBase.cs +++ b/Server/Components/AuthComponentBase.cs @@ -3,26 +3,43 @@ using Microsoft.AspNetCore.Identity; using Remotely.Server.Services; using Remotely.Shared.Models; +using System; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; namespace Remotely.Server.Components; public class AuthComponentBase : ComponentBase { + private RemotelyUser? _user; + private string? _userName; + protected override async Task OnInitializedAsync() { IsAuthenticated = await AuthService.IsAuthenticated(); - User = await AuthService.GetUser(); - Username = User?.UserName; + var userResult = await AuthService.GetUser(); + if (userResult.IsSuccess) + { + _user = userResult.Value; + _userName = userResult.Value.UserName ?? string.Empty; + } await base.OnInitializedAsync(); } public bool IsAuthenticated { get; private set; } - public RemotelyUser User { get; private set; } + public RemotelyUser User + { + get => _user ?? throw new InvalidOperationException("User has not been resolved yet."); + private set => _user = value; + } - public string Username { get; private set; } + public string UserName + { + get => _userName ?? throw new InvalidOperationException("User has not been resolved yet."); + private set => _userName = value; + } [Inject] - protected IAuthService AuthService { get; set; } + protected IAuthService AuthService { get; set; } = null!; } diff --git a/Server/Components/Devices/DeviceCard.razor.cs b/Server/Components/Devices/DeviceCard.razor.cs index 8c41efafb..cfb503fa5 100644 --- a/Server/Components/Devices/DeviceCard.razor.cs +++ b/Server/Components/Devices/DeviceCard.razor.cs @@ -80,7 +80,7 @@ protected override async Task OnInitializedAsync() await base.OnInitializedAsync(); _theme = await AppState.GetEffectiveTheme(); _currentVersion = UpgradeService.GetCurrentVersion(); - _deviceGroups = DataService.GetDeviceGroups(Username); + _deviceGroups = DataService.GetDeviceGroups(UserName); AppState.PropertyChanged += AppState_PropertyChanged; CircuitConnection.MessageReceived += CircuitConnection_MessageReceived; } diff --git a/Server/Components/Devices/DevicesFrame.razor.cs b/Server/Components/Devices/DevicesFrame.razor.cs index fd0ac926c..3d2cd22fb 100644 --- a/Server/Components/Devices/DevicesFrame.razor.cs +++ b/Server/Components/Devices/DevicesFrame.razor.cs @@ -34,29 +34,26 @@ public partial class DevicesFrame : AuthComponentBase, IDisposable private readonly List _sortableProperties = new(); private int _currentPage = 1; private int _devicesPerPage = 25; - private string _filter; + private string? _filter; private bool _hideOfflineDevices = true; - private string _selectedGroupId; + private string? _selectedGroupId; private string _selectedSortProperty = "DeviceName"; private ListSortDirection _sortDirection; [Inject] - private IClientAppState AppState { get; set; } + private IClientAppState AppState { get; init; } = null!; [Inject] - private ICircuitConnection CircuitConnection { get; set; } + private ICircuitConnection CircuitConnection { get; init; } = null!; [Inject] - private IDataService DataService { get; set; } + private IDataService DataService { get; init; } = null!; [Inject] - private IJsInterop JsInterop { get; set; } + private IJsInterop JsInterop { get; init; } = null!; [Inject] - private ILogger Logger { get; set; } - - [Inject] - private IToastService ToastService { get; set; } + private IToastService ToastService { get; init; } = null!; private int TotalPages => (int)Math.Max(1, Math.Ceiling((decimal)_filteredDevices.Count / _devicesPerPage)); @@ -82,7 +79,7 @@ protected override async Task OnInitializedAsync() _deviceGroups.Clear(); - _deviceGroups.AddRange(DataService.GetDeviceGroups(User.UserName)); + _deviceGroups.AddRange(DataService.GetDeviceGroups(UserName)); _selectedGroupId = _deviceGroupAll; @@ -105,23 +102,31 @@ protected override bool ShouldRender() return shouldRender; } - private void AddScriptResult(ScriptResult result) + private async Task AddScriptResult(ScriptResult result) { - var device = DataService.GetDevice(result.DeviceID); - AppState.AddTerminalLine($"{device?.DeviceName} @ {result.TimeStamp}", "font-weight-bold"); + var deviceResult = await DataService.GetDevice(result.DeviceID); + if (!deviceResult.IsSuccess) + { + return; + } - foreach (var line in result.StandardOutput) + AppState.AddTerminalLine($"{deviceResult.Value.DeviceName} @ {result.TimeStamp}", "font-weight-bold"); + + var stdOut = result.StandardOutput ?? Array.Empty(); + var stdErr = result.ErrorOutput ?? Array.Empty(); + + foreach (var line in stdOut) { AppState.AddTerminalLine(line, "text-info"); } - foreach (var line in result.ErrorOutput) + foreach (var line in stdErr) { AppState.AddTerminalLine(line, "text-danger"); } AppState.InvokePropertyChanged(nameof(AppState.TerminalLines)); } - private void AppState_PropertyChanged(object sender, PropertyChangedEventArgs e) + private void AppState_PropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(AppState.DevicesFrameFocusedCardState) || e.PropertyName == nameof(AppState.DevicesFrameFocusedDevice) || @@ -131,7 +136,7 @@ private void AppState_PropertyChanged(object sender, PropertyChangedEventArgs e) } } - private void CircuitConnection_MessageReceived(object sender, CircuitEvent args) + private async void CircuitConnection_MessageReceived(object? sender, CircuitEvent args) { switch (args.EventName) { @@ -165,13 +170,15 @@ private void CircuitConnection_MessageReceived(object sender, CircuitEvent args) var className = (string)args.Params[2]; AppState.AddTerminalLine(terminalMessage); ToastService.ShowToast(toastMessage, classString: className); - InvokeAsync(StateHasChanged); + await InvokeAsync(StateHasChanged); } break; case CircuitEventName.ScriptResult: { - var result = (ScriptResult)args.Params[0]; - AddScriptResult(result); + if (args.Params[0] is ScriptResult result) + { + await AddScriptResult(result); + } } break; default: @@ -181,7 +188,7 @@ private void CircuitConnection_MessageReceived(object sender, CircuitEvent args) private void ClearSelectedCard() { - AppState.DevicesFrameFocusedDevice = null; + AppState.DevicesFrameFocusedDevice = string.Empty; AppState.DevicesFrameFocusedCardState = DeviceCardState.Normal; } @@ -238,8 +245,8 @@ private void FilterDevices() var propInfo = _sortableProperties.Find(x => x.Name == _selectedSortProperty); - var valueA = propInfo.GetValue(a); - var valueB = propInfo.GetValue(b); + var valueA = propInfo?.GetValue(a); + var valueB = propInfo?.GetValue(b); return Comparer.Default.Compare(valueA, valueB) * direction; }); @@ -280,7 +287,7 @@ private void LoadDevices() { _allDevices.Clear(); - var devices = DataService.GetDevicesForUser(Username) + var devices = DataService.GetDevicesForUser(UserName) .OrderByDescending(x => x.IsOnline) .ToList(); @@ -340,7 +347,7 @@ private void ToggleSortDirection() private async Task WakeDevices() { var offlineDevices = DataService - .GetDevicesForUser(Username) + .GetDevicesForUser(UserName) .Where(x => !x.IsOnline); if (_selectedGroupId == _deviceGroupNone) diff --git a/Server/Components/Devices/Terminal.razor.cs b/Server/Components/Devices/Terminal.razor.cs index 82bf98f9f..0280b7ebd 100644 --- a/Server/Components/Devices/Terminal.razor.cs +++ b/Server/Components/Devices/Terminal.razor.cs @@ -160,12 +160,25 @@ private void CircuitConnection_MessageReceived(object sender, Models.CircuitEven AppState.InvokePropertyChanged(nameof(AppState.TerminalLines)); } } - private void DisplayCompletions(List completionMatches) + private async Task DisplayCompletions(List completionMatches) { var deviceId = AppState.DevicesFrameSelectedDevices.FirstOrDefault(); - var device = DataService.GetDevice(deviceId); + if (string.IsNullOrWhiteSpace(deviceId)) + { + return; + } + + var deviceResult = await DataService.GetDevice(deviceId); + + if (!deviceResult.IsSuccess) + { + ToastService.ShowToast2(deviceResult.Reason, Enums.ToastType.Warning); + return; + } - AppState.AddTerminalLine($"Completions for {device?.DeviceName}", className: "font-weight-bold"); + AppState.AddTerminalLine( + $"Completions for {deviceResult.Value.DeviceName}", + className: "font-weight-bold"); foreach (var match in completionMatches) { diff --git a/Server/Components/Scripts/RunScript.razor.cs b/Server/Components/Scripts/RunScript.razor.cs index f5c0ce25d..755926891 100644 --- a/Server/Components/Scripts/RunScript.razor.cs +++ b/Server/Components/Scripts/RunScript.razor.cs @@ -31,25 +31,25 @@ public partial class RunScript : AuthComponentBase private bool _runOnNextConnect = true; - private SavedScript _selectedScript; + private SavedScript? _selectedScript; [Inject] - private IDataService DataService { get; set; } + private IDataService DataService { get; init; } = null!; [Inject] - private IJsInterop JsInterop { get; set; } + private IJsInterop JsInterop { get; init; } = null!; [Inject] - private IToastService ToastService { get; set; } + private IToastService ToastService { get; init; } = null!; [Inject] - private IAgentHubSessionCache ServiceSessionCache { get; init; } + private IAgentHubSessionCache ServiceSessionCache { get; init; } = null!; [Inject] - private ICircuitConnection CircuitConnection { get; set; } + private ICircuitConnection CircuitConnection { get; init; } = null!; [CascadingParameter] - private ScriptsPage ParentPage { get; set; } + private ScriptsPage ParentPage { get; init; } = null!; protected override void OnAfterRender(bool firstRender) { @@ -64,16 +64,20 @@ protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); - _deviceGroups = DataService.GetDeviceGroups(User.UserName); + _deviceGroups = DataService.GetDeviceGroups(UserName); _devices = DataService - .GetDevicesForUser(User.UserName) + .GetDevicesForUser(UserName) .OrderBy(x => x.DeviceName) .ToArray(); } private void DeviceGroupSelectedChanged(ChangeEventArgs args, DeviceGroup deviceGroup) { - var isSelected = (bool)args.Value; + if (args.Value is not bool isSelected) + { + return; + } + if (isSelected) { _selectedDeviceGroups.Add(deviceGroup.ID); @@ -86,7 +90,11 @@ private void DeviceGroupSelectedChanged(ChangeEventArgs args, DeviceGroup device private void DeviceSelectedChanged(ChangeEventArgs args, Device device) { - var isSelected = (bool)args.Value; + if (args.Value is not bool isSelected) + { + return; + } + if (isSelected) { _selectedDevices.Add(device.ID); @@ -113,7 +121,7 @@ private async Task ExecuteScript() } var deviceIdsFromDeviceGroups = _devices - .Where(x => _selectedDeviceGroups.Contains(x.DeviceGroupID)) + .Where(x => _selectedDeviceGroups.Contains(x.DeviceGroupID!)) .Select(x => x.ID); var deviceIds = _selectedDevices @@ -121,7 +129,7 @@ private async Task ExecuteScript() .Distinct() .ToArray(); - var filteredDevices = DataService.FilterDeviceIDsByUserPermission(deviceIds.ToArray(), User); + var filteredDevices = DataService.FilterDeviceIdsByUserPermission(deviceIds.ToArray(), User); var onlineDevices = ServiceSessionCache.FilterDevicesByOnlineStatus(filteredDevices, true); @@ -157,7 +165,11 @@ private async Task ScriptSelected(ScriptTreeNode viewModel) { if (viewModel.Script is not null) { - _selectedScript = await DataService.GetSavedScript(User.Id, viewModel.Script.Id); + var scriptResult = await DataService.GetSavedScript(User.Id, viewModel.Script.Id); + if (scriptResult.IsSuccess) + { + _selectedScript = scriptResult.Value; + } } else { diff --git a/Server/Components/Scripts/SavedScripts.razor.cs b/Server/Components/Scripts/SavedScripts.razor.cs index 17b5754b4..a9e4b5e05 100644 --- a/Server/Components/Scripts/SavedScripts.razor.cs +++ b/Server/Components/Scripts/SavedScripts.razor.cs @@ -17,24 +17,24 @@ namespace Remotely.Server.Components.Scripts; public partial class SavedScripts : AuthComponentBase { [CascadingParameter] - private ScriptsPage ParentPage { get; set; } + private ScriptsPage ParentPage { get; set; } = null!; private SavedScript _selectedScript = new() { Name = "Test Script" }; - private string _alertMessage; - private string _alertOptionsShowClass; - private string _environmentVarsShowClass; + private string _alertMessage = string.Empty; + private string _alertOptionsShowClass = string.Empty; + private string _environmentVarsShowClass = string.Empty; [Inject] - public IDataService DataService { get; set; } + public IDataService DataService { get; set; } = null!; [Inject] - public IToastService ToastService { get; set; } + public IToastService ToastService { get; set; } = null!; [Inject] - public IJsInterop JsInterop { get; set; } + public IJsInterop JsInterop { get; set; } = null!; [Inject] - public IModalService ModalService { get; set; } + public IModalService ModalService { get; set; } = null!; private bool CanModifyScript => _selectedScript.Id == Guid.Empty || _selectedScript.CreatorId == User.Id || User.IsAdministrator; @@ -104,18 +104,18 @@ private async Task ScriptSelected(ScriptTreeNode viewModel) { if (viewModel.Script is not null) { - _selectedScript = await DataService.GetSavedScript(User.Id, viewModel.Script.Id) ?? new() + var result = await DataService.GetSavedScript(User.Id, viewModel.Script.Id); + if (result.IsSuccess) { - Name = "Test Script" - }; + _selectedScript = result.Value; + return; + } } - else + + _selectedScript = new() { - _selectedScript = new() - { - Name = "Test Script" - }; - } + Name = string.Empty + }; } private void ToggleEnvironmentVarsShown() diff --git a/Server/Components/Scripts/ScriptSchedules.razor b/Server/Components/Scripts/ScriptSchedules.razor index db84391c2..fd5e727d6 100644 --- a/Server/Components/Scripts/ScriptSchedules.razor +++ b/Server/Components/Scripts/ScriptSchedules.razor @@ -13,12 +13,12 @@
- +
- @@ -26,7 +26,7 @@ New diff --git a/Server/Components/Scripts/ScriptSchedules.razor.cs b/Server/Components/Scripts/ScriptSchedules.razor.cs index a9fd5af5d..12dfc9e63 100644 --- a/Server/Components/Scripts/ScriptSchedules.razor.cs +++ b/Server/Components/Scripts/ScriptSchedules.razor.cs @@ -21,13 +21,13 @@ public partial class ScriptSchedules : AuthComponentBase private readonly List _schedules = new(); - private string _alertMessage; + private string _alertMessage = string.Empty; private DeviceGroup[] _deviceGroups = Array.Empty(); private Device[] _devices = Array.Empty(); - private SavedScript _selectedScript; + private SavedScript? _selectedScript; private ScriptSchedule _selectedSchedule = new() { @@ -36,33 +36,37 @@ public partial class ScriptSchedules : AuthComponentBase }; [CascadingParameter] - private ScriptsPage ParentPage { get; set; } + private ScriptsPage ParentPage { get; set; } = null!; [Inject] - private IDataService DataService { get; set; } + private IDataService DataService { get; set; } = null!; [Inject] - private IJsInterop JsInterop { get; set; } + private IJsInterop JsInterop { get; set; } = null!; [Inject] - private IToastService ToastService { get; set; } + private IToastService ToastService { get; set; } = null!; - private bool CanModifyScript => string.IsNullOrWhiteSpace(_selectedSchedule.CreatorId) || - _selectedSchedule.CreatorId == User.Id || - User.IsAdministrator; + private bool CanModifySchedule => + _selectedSchedule.CreatorId == User?.Id || + User?.IsAdministrator == true; - private bool CanDeleteScript => !string.IsNullOrWhiteSpace(_selectedSchedule.CreatorId) && - (_selectedSchedule.CreatorId == User.Id || User.IsAdministrator); + private bool CanDeleteSchedule => + _selectedSchedule.CreatorId == User?.Id || + User?.IsAdministrator == true; protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); - _deviceGroups = DataService.GetDeviceGroups(User.UserName); - _devices = DataService - .GetDevicesForUser(User.UserName) - .OrderBy(x => x.DeviceName) - .ToArray(); + if (IsAuthenticated) + { + _deviceGroups = DataService.GetDeviceGroups(UserName); + _devices = DataService + .GetDevicesForUser(UserName) + .OrderBy(x => x.DeviceName) + .ToArray(); + } await RefreshSchedules(); } @@ -84,7 +88,7 @@ private void CreateNew() private async Task DeleteSelectedSchedule() { - if (User.Id != _selectedSchedule.CreatorId) + if (User?.Id != _selectedSchedule.CreatorId) { ToastService.ShowToast("You can't delete other people's script schedules.", classString: "bg-warning"); return; @@ -104,7 +108,10 @@ private async Task DeleteSelectedSchedule() private void DeviceGroupSelectedChanged(ChangeEventArgs args, DeviceGroup deviceGroup) { - var isSelected = (bool)args.Value; + if (args.Value is not bool isSelected) + { + return; + } if (isSelected) { _selectedDeviceGroups.Add(deviceGroup.ID); @@ -117,7 +124,10 @@ private void DeviceGroupSelectedChanged(ChangeEventArgs args, DeviceGroup device private void DeviceSelectedChanged(ChangeEventArgs args, Device device) { - var isSelected = (bool)args.Value; + if (args.Value is not bool isSelected) + { + return; + } if (isSelected) { _selectedDevices.Add(device.ID); @@ -141,7 +151,7 @@ private async Task OnValidSubmit(EditContext context) return; } - if (!CanModifyScript) + if (!CanModifySchedule) { ToastService.ShowToast("You can't modify other people's schedules.", classString: "bg-warning"); return; @@ -177,7 +187,10 @@ private async Task OnValidSubmit(EditContext context) private async Task RefreshSchedules() { _schedules.Clear(); - _schedules.AddRange(await DataService.GetScriptSchedules(User.OrganizationID)); + if (User is not null) + { + _schedules.AddRange(await DataService.GetScriptSchedules(User.OrganizationID)); + } } private string GetTableRowClass(ScriptSchedule schedule) @@ -203,14 +216,23 @@ private async Task SelectTableRow(ScriptSchedule schedule) { _selectedDeviceGroups.AddRange(schedule.DeviceGroups.Select(x => x.ID)); } - _selectedScript = await DataService.GetSavedScript(_selectedSchedule.SavedScriptId); + + var result = await DataService.GetSavedScript(_selectedSchedule.SavedScriptId); + if (result.IsSuccess) + { + _selectedScript = result.Value; + } } private async Task ScriptSelected(ScriptTreeNode viewModel) { if (viewModel.Script is not null) { - _selectedScript = await DataService.GetSavedScript(User.Id, viewModel.Script.Id); + var result = await DataService.GetSavedScript(User.Id, viewModel.Script.Id); + if (result.IsSuccess) + { + _selectedScript = result.Value; + } } else diff --git a/Server/Components/Scripts/ScriptTreeNode.cs b/Server/Components/Scripts/ScriptTreeNode.cs index ce683acc6..e3d9e76a5 100644 --- a/Server/Components/Scripts/ScriptTreeNode.cs +++ b/Server/Components/Scripts/ScriptTreeNode.cs @@ -11,8 +11,8 @@ public class ScriptTreeNode { public string Id { get; } = Guid.NewGuid().ToString(); public TreeItemType ItemType { get; set; } - public string Name { get; init; } + public string Name { get; init; } = string.Empty; public ScriptTreeNode? ParentNode { get; set; } public List ChildItems { get; } = new(); - public SavedScript Script { get; init; } + public SavedScript? Script { get; init; } } diff --git a/Server/Data/AppDb.cs b/Server/Data/AppDb.cs index 6e9d07dd0..9a3deff42 100644 --- a/Server/Data/AppDb.cs +++ b/Server/Data/AppDb.cs @@ -135,6 +135,10 @@ protected override void OnModelCreating(ModelBuilder builder) builder.Entity() .HasMany(x => x.ScriptSchedules) .WithMany(x => x.Devices); + builder.Entity() + .HasOne(x => x.DeviceGroup) + .WithMany(x => x.Devices) + .IsRequired(false); builder.Entity() .Property(x => x.MacAddresses) .HasConversion( @@ -143,7 +147,9 @@ protected override void OnModelCreating(ModelBuilder builder) valueComparer: _stringArrayComparer); builder.Entity() - .HasMany(x => x.Devices); + .HasMany(x => x.Devices) + .WithOne(x => x.DeviceGroup) + .IsRequired(false); builder.Entity() .HasMany(x => x.ScriptSchedules) .WithMany(x => x.DeviceGroups); diff --git a/Server/Hubs/AgentHub.cs b/Server/Hubs/AgentHub.cs index a881d569d..811620c33 100644 --- a/Server/Hubs/AgentHub.cs +++ b/Server/Hubs/AgentHub.cs @@ -255,10 +255,15 @@ public Task ReturnPowerShellCompletions(PwshCommandCompletion completion, Comple return _circuitManager.InvokeOnConnection(senderConnectionId, CircuitEventName.PowerShellCompletions, completion, intent); } - public Task ScriptResult(string scriptResultId) + public async Task ScriptResult(string scriptResultId) { - var result = _dataService.GetScriptResult(scriptResultId); - return _circuitManager.InvokeOnConnection(result.SenderConnectionID, + var result = await _dataService.GetScriptResult(scriptResultId); + if (!result.IsSuccess) + { + return; + } + + _ = await _circuitManager.InvokeOnConnection($"{result.Value.SenderConnectionID}", CircuitEventName.ScriptResult, result); } diff --git a/Server/Hubs/CircuitConnection.cs b/Server/Hubs/CircuitConnection.cs index 4d452c888..bde56c2bd 100644 --- a/Server/Hubs/CircuitConnection.cs +++ b/Server/Hubs/CircuitConnection.cs @@ -27,8 +27,8 @@ namespace Remotely.Server.Hubs; public interface ICircuitConnection { - event EventHandler MessageReceived; - RemotelyUser User { get; } + event EventHandler? MessageReceived; + RemotelyUser? User { get; } Task DeleteRemoteLogs(string deviceId); @@ -85,6 +85,8 @@ public class CircuitConnection : CircuitHandler, ICircuitConnection private readonly ILogger _logger; private readonly IAgentHubSessionCache _agentSessionCache; private readonly IToastService _toastService; + private RemotelyUser? _user; + public CircuitConnection( IAuthService authService, IDataService dataService, @@ -112,10 +114,15 @@ public CircuitConnection( } - public event EventHandler MessageReceived; + public event EventHandler? MessageReceived; + + public string ConnectionId { get; } = Guid.NewGuid().ToString(); - public string ConnectionId { get; set; } - public RemotelyUser User { get; internal set; } + public RemotelyUser User + { + get => _user ?? throw new InvalidOperationException("User has not been resolved yet."); + internal set => _user = value; + } public Task DeleteRemoteLogs(string deviceId) @@ -129,14 +136,14 @@ public Task DeleteRemoteLogs(string deviceId) _logger.LogInformation("Delete logs command sent. Device: {deviceId}. User: {username}", deviceId, - User.UserName); + User?.UserName); return _agentHubContext.Clients.Client(key).SendAsync("DeleteLogs"); } public Task ExecuteCommandOnAgent(ScriptingShell shell, string command, string[] deviceIDs) { - deviceIDs = _dataService.FilterDeviceIDsByUserPermission(deviceIDs, User); + deviceIDs = _dataService.FilterDeviceIdsByUserPermission(deviceIDs, User); var connections = GetActiveConnectionsForUserOrg(deviceIDs); _logger.LogInformation("Command executed by {username}. Shell: {shell}. Command: {command}. Devices: {deviceIds}", @@ -162,7 +169,13 @@ public Task ExecuteCommandOnAgent(ScriptingShell shell, string command, string[] public Task GetPowerShellCompletions(string inputText, int currentIndex, CompletionIntent intent, bool? forward) { - var (canAccess, key) = CanAccessDevice(_appState.DevicesFrameSelectedDevices.FirstOrDefault()); + var device = _appState.DevicesFrameSelectedDevices.FirstOrDefault(); + if (device is null) + { + return Task.CompletedTask; + } + + var (canAccess, key) = CanAccessDevice(device); if (!canAccess) { return Task.CompletedTask; @@ -202,8 +215,13 @@ public override async Task OnCircuitOpenedAsync(Circuit circuit, CancellationTok { if (await _authService.IsAuthenticated()) { - User = await _authService.GetUser(); - ConnectionId = Guid.NewGuid().ToString(); + var userResult = await _authService.GetUser(); + if (!userResult.IsSuccess) + { + _toastService.ShowToast2("Authorization failure.", Enums.ToastType.Error); + return; + } + _user = userResult.Value; _circuitManager.TryAddConnection(ConnectionId, this); } await base.OnCircuitOpenedAsync(circuit, cancellationToken); @@ -211,7 +229,7 @@ public override async Task OnCircuitOpenedAsync(Circuit circuit, CancellationTok public Task ReinstallAgents(string[] deviceIDs) { - deviceIDs = _dataService.FilterDeviceIDsByUserPermission(deviceIDs, User); + deviceIDs = _dataService.FilterDeviceIdsByUserPermission(deviceIDs, User); var connections = GetActiveConnectionsForUserOrg(deviceIDs); foreach (var connection in connections) { @@ -283,12 +301,20 @@ public async Task> RemoteControl(string deviceId, _remoteControlSessionCache.AddOrUpdate($"{sessionId}", session); - var organization = _dataService.GetOrganizationNameByUserName(User.UserName); + var orgResult = await _dataService.GetOrganizationNameByUserName($"{User.UserName}"); + + if (!orgResult.IsSuccess) + { + _toastService.ShowToast2(orgResult.Reason, Enums.ToastType.Warning); + return Result.Fail(orgResult.Reason); + } + + var organization = _dataService.GetOrganizationNameByUserName($"{User.UserName}"); await _agentHubContext.Clients.Client(serviceConnectionId).SendAsync("RemoteControl", sessionId, accessKey, ConnectionId, - User.UserOptions.DisplayName, + User.UserOptions?.DisplayName, organization, User.OrganizationID); @@ -297,22 +323,31 @@ await _agentHubContext.Clients.Client(serviceConnectionId).SendAsync("RemoteCont public Task RemoveDevices(string[] deviceIDs) { - var filterDevices = _dataService.FilterDeviceIDsByUserPermission(deviceIDs, User); - _dataService.RemoveDevices(filterDevices); + if (User is not null) + { + var filterDevices = _dataService.FilterDeviceIdsByUserPermission(deviceIDs, User); + _dataService.RemoveDevices(filterDevices); + } + return Task.CompletedTask; } - public async Task RunScript(IEnumerable deviceIds, Guid savedScriptId, int scriptRunId, ScriptInputType scriptInputType, bool runAsHostedService) + public async Task RunScript( + IEnumerable deviceIds, + Guid savedScriptId, + int scriptRunId, + ScriptInputType scriptInputType, + bool runAsHostedService) { - string username; + var username = string.Empty; if (runAsHostedService) { username = "Remotely Server"; } - else + else if (User is not null) { username = User.UserName; - deviceIds = _dataService.FilterDeviceIDsByUserPermission(deviceIds.ToArray(), User); + deviceIds = _dataService.FilterDeviceIdsByUserPermission(deviceIds.ToArray(), User); } var authToken = _expiringTokenService.GetToken(Time.Now.AddMinutes(AppConstants.ScriptRunExpirationMinutes)); @@ -326,32 +361,37 @@ public async Task RunScript(IEnumerable deviceIds, Guid savedScriptId, i } - public Task SendChat(string message, string deviceId) + public async Task SendChat(string message, string deviceId) { if (!_dataService.DoesUserHaveAccessToDevice(deviceId, User)) { - return Task.CompletedTask; + return; } if (!_agentSessionCache.TryGetByDeviceId(deviceId, out var device) || !_agentSessionCache.TryGetConnectionId(deviceId, out var connectionId)) { _toastService.ShowToast("Device not found."); - return Task.CompletedTask; + return; } if (device.OrganizationID != User.OrganizationID) { _toastService.ShowToast("Unauthorized."); - return Task.CompletedTask; + return; } - var organizationName = _dataService.GetOrganizationNameByUserName(User.UserName); + var orgResult = await _dataService.GetOrganizationNameByUserName($"{User.UserName}"); + if (!orgResult.IsSuccess) + { + _toastService.ShowToast2("Organization not found.", Enums.ToastType.Warning); + return; + } - return _agentHubContext.Clients.Client(connectionId).SendAsync("Chat", - User.UserOptions.DisplayName ?? User.UserName, + await _agentHubContext.Clients.Client(connectionId).SendAsync("Chat", + User.UserOptions?.DisplayName ?? User.UserName, message, - organizationName, + orgResult.Value, User.OrganizationID, false, ConnectionId); @@ -397,16 +437,15 @@ public async Task TriggerHeartbeat(string deviceId) await _agentHubContext.Clients.Client(connectionId).SendAsync("TriggerHeartbeat"); } - public Task UninstallAgents(string[] deviceIDs) + public async Task UninstallAgents(string[] deviceIDs) { - deviceIDs = _dataService.FilterDeviceIDsByUserPermission(deviceIDs, User); + deviceIDs = _dataService.FilterDeviceIdsByUserPermission(deviceIDs, User); var connections = GetActiveConnectionsForUserOrg(deviceIDs); foreach (var connection in connections) { - _agentHubContext.Clients.Client(connection).SendAsync("UninstallAgent"); + await _agentHubContext.Clients.Client(connection).SendAsync("UninstallAgent"); } _dataService.RemoveDevices(deviceIDs); - return Task.CompletedTask; } public Task UpdateTags(string deviceID, string tags) @@ -436,7 +475,7 @@ public Task UploadFiles(List fileIDs, string transferID, string[] device User.UserName, string.Join(", ", fileIDs)); - deviceIDs = _dataService.FilterDeviceIDsByUserPermission(deviceIDs, User); + deviceIDs = _dataService.FilterDeviceIdsByUserPermission(deviceIDs, User); var connections = GetActiveConnectionsForUserOrg(deviceIDs); foreach (var connection in connections) { @@ -477,7 +516,7 @@ public async Task WakeDevices(Device[] devices) try { var deviceIds = devices.Select(x => x.ID).ToArray(); - var filteredIds = _dataService.FilterDeviceIDsByUserPermission(deviceIds, User); + var filteredIds = _dataService.FilterDeviceIdsByUserPermission(deviceIds, User); var filteredDevices = devices.Where(x => filteredIds.Contains(x.ID)).ToArray(); var availableDevices = _agentSessionCache diff --git a/Server/Pages/ApiKeys.razor b/Server/Pages/ApiKeys.razor index 2938ebba2..2386ad500 100644 --- a/Server/Pages/ApiKeys.razor +++ b/Server/Pages/ApiKeys.razor @@ -93,7 +93,7 @@ else var secret = RandomGenerator.GenerateString(36); var secretHash = new PasswordHasher().HashPassword(string.Empty, secret); - var result = await DataService.CreateApiToken(Username, _createKeyName, secretHash); + var result = await DataService.CreateApiToken(UserName, _createKeyName, secretHash); if (!result.IsSuccess) { ToastService.ShowToast2(result.Reason, ToastType.Error); @@ -112,7 +112,7 @@ else return; } - var deleteResult = await DataService.DeleteApiToken(Username, keyId); + var deleteResult = await DataService.DeleteApiToken(UserName, keyId); if (!deleteResult.IsSuccess) { ToastService.ShowToast2(deleteResult.Reason, ToastType.Error); @@ -137,7 +137,7 @@ else var newName = await JsInterop.Prompt("New key name"); if (!string.IsNullOrWhiteSpace(newName)) { - await DataService.RenameApiToken(Username, keyId, newName); + await DataService.RenameApiToken(UserName, keyId, newName); RefreshData(); _alertMessage = "Key renamed."; } diff --git a/Server/Pages/Branding.razor b/Server/Pages/Branding.razor index ac747c9ca..2b9a26f72 100644 --- a/Server/Pages/Branding.razor +++ b/Server/Pages/Branding.razor @@ -129,14 +129,26 @@ private async Task LoadBrandingInfo() { - var organization = await DataService.GetOrganizationByUserName(User.UserName); + var orgResult = await DataService.GetOrganizationByUserName($"{UserName}"); - var brandingInfo = await DataService.GetBrandingInfo(organization.ID); + if (!orgResult.IsSuccess) + { + ToastService.ShowToast2(orgResult.Reason, ToastType.Warning); + return; + } + + var brandingResult= await DataService.GetBrandingInfo(orgResult.Value.ID); + if (!brandingResult.IsSuccess) + { + ToastService.ShowToast2(brandingResult.Reason, ToastType.Warning); + return; + } + var brandingInfo = brandingResult.Value; _inputModel.ProductName = brandingInfo.Product; - if (brandingInfo?.Icon?.Any() == true) + if (brandingResult.Value?.Icon?.Any() == true) { _base64Icon = Convert.ToBase64String(brandingInfo.Icon); } diff --git a/Server/Pages/DeviceDetails.razor b/Server/Pages/DeviceDetails.razor index b52a521cc..f8979bc2c 100644 --- a/Server/Pages/DeviceDetails.razor +++ b/Server/Pages/DeviceDetails.razor @@ -29,11 +29,11 @@
} -else if (Device is null) +else if (_device is null) {

Device not found.

} -else if (!DataService.DoesUserHaveAccessToDevice(Device.ID, User)) +else if (!DataService.DoesUserHaveAccessToDevice(_device.ID, User)) {

Unauthorized.

} @@ -59,7 +59,7 @@ else Device Details - + @@ -72,31 +72,31 @@ else
- +
- +
- +
- +
- +
@@ -107,7 +107,7 @@ else
- +
@@ -115,7 +115,7 @@ else Total Storage:
- +
@@ -123,7 +123,7 @@ else Memory:
- +
@@ -131,7 +131,7 @@ else Total Memory:
- +
@@ -139,7 +139,7 @@ else Public IP:
- +
@@ -152,42 +152,42 @@ else class="form-control" name="agent-memory-total" readonly - value="@(string.Join(", ", Device.MacAddresses ?? Array.Empty()))" /> + value="@(string.Join(", ", _device.MacAddresses ?? Array.Empty()))" />
- - + +
- + - @foreach (var group in DataService.GetDeviceGroups(Username)) + @foreach (var group in DataService.GetDeviceGroups(UserName)) { } - +
- - + +
- - + +
@@ -199,7 +199,7 @@ else
- @if (!Device.IsOnline) + @if (!_device.IsOnline) {
Device must be online to retrieve logs.
} diff --git a/Server/Pages/DeviceDetails.razor.cs b/Server/Pages/DeviceDetails.razor.cs index 5f9cc4aa3..830fe90c8 100644 --- a/Server/Pages/DeviceDetails.razor.cs +++ b/Server/Pages/DeviceDetails.razor.cs @@ -20,33 +20,34 @@ public partial class DeviceDetails : AuthComponentBase private readonly ConcurrentQueue _logLines = new(); private readonly ConcurrentQueue _scriptResults = new(); - private string _alertMessage; - private string _inputDeviceId; + private string? _alertMessage; + private Device? _device; + private string? _inputDeviceId; [Parameter] - public string ActiveTab { get; set; } + public string ActiveTab { get; set; } = string.Empty; [Parameter] - public string DeviceId { get; set; } + public string DeviceId { get; set; } = string.Empty; + [Inject] - private ICircuitConnection CircuitConnection { get; set; } + private ICircuitConnection CircuitConnection { get; set; } = null!; [Inject] - private IDataService DataService { get; set; } + private IDataService DataService { get; set; } = null!; - private Device Device { get; set; } [Inject] - private IJsInterop JsInterop { get; set; } + private IJsInterop JsInterop { get; set; } = null!; [Inject] - private IModalService ModalService { get; set; } + private IModalService ModalService { get; set; } = null!; [Inject] - private NavigationManager NavManager { get; set; } + private NavigationManager NavManager { get; set; } = null!; [Inject] - private IToastService ToastService { get; set; } + private IToastService ToastService { get; set; } = null!; protected override async Task OnInitializedAsync() @@ -55,13 +56,21 @@ protected override async Task OnInitializedAsync() if (!string.IsNullOrWhiteSpace(DeviceId)) { - Device = DataService.GetDevice(DeviceId); + var deviceResult = await DataService.GetDevice(DeviceId); + if (deviceResult.IsSuccess) + { + _device = deviceResult.Value; + } + else + { + ToastService.ShowToast2(deviceResult.Reason, Enums.ToastType.Warning); + } } CircuitConnection.MessageReceived += CircuitConnection_MessageReceived; } - private void CircuitConnection_MessageReceived(object sender, Models.CircuitEvent e) + private void CircuitConnection_MessageReceived(object? sender, Models.CircuitEvent e) { if (e.EventName == Models.CircuitEventName.RemoteLogsReceived) { @@ -73,10 +82,15 @@ private void CircuitConnection_MessageReceived(object sender, Models.CircuitEven private async Task DeleteLogs() { + if (_device is null) + { + return; + } + var result = await JsInterop.Confirm("Are you sure you want to delete the remote logs?"); if (result) { - await CircuitConnection.DeleteRemoteLogs(Device.ID); + await CircuitConnection.DeleteRemoteLogs(_device.ID); ToastService.ShowToast("Delete command sent."); } } @@ -96,22 +110,32 @@ private void EvaluateDeviceIdInputKeyDown(KeyboardEventArgs args) private void GetRemoteLogs() { + if (_device is null) + { + return; + } + _logLines.Clear(); - if (Device.IsOnline) + if (_device.IsOnline) { - CircuitConnection.GetRemoteLogs(Device.ID); + CircuitConnection.GetRemoteLogs(_device.ID); } } private void GetScriptHistory() { + if (_device is null) + { + return; + } + _scriptResults.Clear(); if (User.IsAdministrator) { var results = DataService - .GetAllScriptResults(User.OrganizationID, Device.ID) + .GetAllScriptResults(User.OrganizationID, _device.ID) .OrderByDescending(x => x.TimeStamp); foreach (var result in results) @@ -122,7 +146,7 @@ private void GetScriptHistory() else { var results = DataService - .GetAllCommandResultsForUser(User.OrganizationID, User.UserName, Device.ID) + .GetAllCommandResultsForUser(User.OrganizationID, UserName, _device.ID) .OrderByDescending(x => x.TimeStamp); foreach (var result in results) @@ -154,11 +178,17 @@ private string GetTrimmedText(string[] source, int stringLength) private Task HandleValidSubmit() { - DataService.UpdateDevice(Device.ID, - Device.Tags, - Device.Alias, - Device.DeviceGroupID, - Device.Notes); + if (_device is null) + { + return Task.CompletedTask; + } + + DataService.UpdateDevice( + _device.ID, + _device.Tags, + _device.Alias, + _device.DeviceGroupID, + _device.Notes); _alertMessage = "Device details saved."; ToastService.ShowToast("Device details saved."); @@ -173,20 +203,25 @@ private void NavigateToDeviceId() private void ShowAllDisks() { - var disksString = JsonSerializer.Serialize(Device.Drives, JsonSerializerHelper.IndentedOptions); + if (_device is null) + { + return; + } + + var disksString = JsonSerializer.Serialize(_device.Drives, JsonSerializerHelper.IndentedOptions); void modalBody(RenderTreeBuilder builder) { builder.AddMarkupContent(0, $"
{disksString}
"); } - ModalService.ShowModal($"All Disks for {Device.DeviceName}", modalBody); + ModalService.ShowModal($"All Disks for {_device.DeviceName}", modalBody); } private void ShowFullScriptOutput(ScriptResult result) { void outputModal(RenderTreeBuilder builder) { - var output = string.Join("\r\n", result.StandardOutput); - var error = string.Join("\r\n", result.ErrorOutput); + var output = string.Join("\r\n", $"{result.StandardOutput}"); + var error = string.Join("\r\n", $"{result.ErrorOutput}"); var textareaStyle = "width: 100%; height: 200px; white-space: pre;"; builder.AddMarkupContent(0, "
Input
"); diff --git a/Server/Pages/Downloads.razor b/Server/Pages/Downloads.razor index 51cebfd09..a48d3e12d 100644 --- a/Server/Pages/Downloads.razor +++ b/Server/Pages/Downloads.razor @@ -2,7 +2,7 @@ @using Microsoft.AspNetCore.Hosting @using Microsoft.Extensions.Logging -@inject AuthenticationStateProvider AuthProvider +@inject IAuthService Auth @inject IDataService DataService @inject UserManager UserManager @inject IWebHostEnvironment HostEnv @@ -174,24 +174,30 @@
@code { - private string _organizationId; + private string? _organizationId; private bool _isAuthenticated; protected override async Task OnInitializedAsync() { - await base.OnInitializedAsync(); - - var authState = await AuthProvider.GetAuthenticationStateAsync(); - _isAuthenticated = authState.User.Identity.IsAuthenticated; + _isAuthenticated = await Auth.IsAuthenticated(); if (_isAuthenticated) { - var currentUser = await DataService.GetUserByName(authState.User.Identity.Name); - _organizationId = currentUser.OrganizationID; + var userResult = await Auth.GetUser(); + if (userResult.IsSuccess) + { + _organizationId = userResult.Value.OrganizationID; + } } else { - _organizationId = (await DataService.GetDefaultOrganization())?.ID; + var orgResult = await DataService.GetDefaultOrganization(); + if (orgResult.IsSuccess) + { + _organizationId = orgResult.Value.ID; + } } + + await base.OnInitializedAsync(); } } \ No newline at end of file diff --git a/Server/Pages/GetSupport.cshtml.cs b/Server/Pages/GetSupport.cshtml.cs index 8233b5c06..216eb0415 100644 --- a/Server/Pages/GetSupport.cshtml.cs +++ b/Server/Pages/GetSupport.cshtml.cs @@ -18,7 +18,7 @@ public GetSupportModel(IDataService dataService, IEmailSenderEx emailSender) } [TempData] - public string StatusMessage { get; set; } + public string? StatusMessage { get; set; } [BindProperty] public InputModel Input { get; set; } @@ -35,8 +35,15 @@ public async Task OnPost(string deviceId) return Page(); } - var orgID = _dataService.GetDevice(deviceId)?.OrganizationID; + var deviceResult = await _dataService.GetDevice(deviceId); + if (!deviceResult.IsSuccess) + { + StatusMessage = "Device not found."; + return Page(); + } + var orgId = deviceResult.Value.OrganizationID; + var alertParts = new string[] { $"{Input.Name} is requesting support.", @@ -47,12 +54,16 @@ public async Task OnPost(string deviceId) }; var alertMessage = string.Join(" ", alertParts); - await _dataService.AddAlert(deviceId, orgID, alertMessage); + await _dataService.AddAlert(deviceId, orgId, alertMessage); - var orgUsers = await _dataService.GetAllUsersInOrganization(orgID); + var orgUsers = await _dataService.GetAllUsersInOrganization(orgId); var emailMessage = string.Join("
", alertParts); foreach (var user in orgUsers) { + if (string.IsNullOrWhiteSpace(user.Email)) + { + continue; + } await _emailSender.SendEmailAsync(user.Email, "Support Request", emailMessage); } @@ -65,9 +76,9 @@ public class InputModel { [StringLength(150)] [Required] - public string Name { get; set; } - public string Email { get; set; } - public string Phone { get; set; } + public required string Name { get; set; } + public string? Email { get; set; } + public string? Phone { get; set; } public bool ChatResponseOk { get; set; } } } \ No newline at end of file diff --git a/Server/Pages/Invite.cshtml b/Server/Pages/Invite.cshtml index 002979e14..aa500e0af 100644 --- a/Server/Pages/Invite.cshtml +++ b/Server/Pages/Invite.cshtml @@ -14,7 +14,12 @@ You've successfully joined the organization!

} - + else if (string.IsNullOrWhiteSpace(Model.Input.InviteID)) + { +
+ Invitation ID is missing from the request. +
+ } else {
diff --git a/Server/Pages/Invite.cshtml.cs b/Server/Pages/Invite.cshtml.cs index 1d53afb38..6ce34c382 100644 --- a/Server/Pages/Invite.cshtml.cs +++ b/Server/Pages/Invite.cshtml.cs @@ -2,22 +2,25 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Remotely.Server.Services; +using System.Threading.Tasks; namespace Remotely.Server.Pages; [Authorize] public class InviteModel : PageModel { + private readonly IDataService _dataService; + public InviteModel(IDataService dataService) { - DataService = dataService; + _dataService = dataService; } - private IDataService DataService { get; } + public bool Success { get; set; } public class InputModel { - public string InviteID { get; set; } + public string? InviteID { get; set; } } [BindProperty] @@ -28,12 +31,13 @@ public void OnGet(string id) if (string.IsNullOrWhiteSpace(id)) { ModelState.AddModelError("MissingID", "No invititation ID is specified."); + return; } Input.InviteID = id; } - public IActionResult OnPost() + public async Task OnPost() { if (string.IsNullOrWhiteSpace(Input?.InviteID)) { @@ -42,8 +46,8 @@ public IActionResult OnPost() return Page(); } - var result = DataService.JoinViaInvitation(User.Identity.Name, Input.InviteID); - if (result == false) + var result = await _dataService.JoinViaInvitation($"{User.Identity?.Name}", Input.InviteID); + if (!result.IsSuccess) { Success = false; ModelState.AddModelError("InviteIDNotFound", "The invitation ID wasn't found or is for another account."); diff --git a/Server/Pages/ManageOrganization.razor.cs b/Server/Pages/ManageOrganization.razor.cs index c43efbc26..992813a7f 100644 --- a/Server/Pages/ManageOrganization.razor.cs +++ b/Server/Pages/ManageOrganization.razor.cs @@ -120,7 +120,7 @@ private async Task DeleteInvite(InviteLink invite) return; } - DataService.DeleteInvite(User.OrganizationID, invite.ID); + await DataService.DeleteInvite(User.OrganizationID, invite.ID); _invites.RemoveAll(x => x.ID == invite.ID); ToastService.ShowToast("Invitation deleted."); } @@ -143,7 +143,7 @@ private async Task DeleteSelectedDeviceGroup() return; } - DataService.DeleteDeviceGroup(User.OrganizationID, _selectedDeviceGroupId); + await DataService.DeleteDeviceGroup(User.OrganizationID, _selectedDeviceGroupId); _deviceGroups.RemoveAll(x => x.ID == _selectedDeviceGroupId); _selectedDeviceGroupId = string.Empty; } @@ -237,14 +237,20 @@ private void OrganizationNameChanged(ChangeEventArgs args) private async Task RefreshData() { - _organization = await DataService.GetOrganizationByUserName(Username); + var orgResult = await DataService.GetOrganizationByUserName(UserName); + if (!orgResult.IsSuccess) + { + ToastService.ShowToast2(orgResult.Reason, Enums.ToastType.Warning); + return; + } + _organization = orgResult.Value; _orgUsers.Clear(); _invites.Clear(); _deviceGroups.Clear(); _invites.AddRange(DataService.GetAllInviteLinks(User.OrganizationID).OrderBy(x => x.InvitedUser)); - _deviceGroups.AddRange(DataService.GetDeviceGroups(Username).OrderBy(x => x.Name)); + _deviceGroups.AddRange(DataService.GetDeviceGroups(UserName).OrderBy(x => x.Name)); var orgUsers = await DataService.GetAllUsersInOrganization(User.OrganizationID); _orgUsers.AddRange(orgUsers.OrderBy(x => x.UserName)); } @@ -277,9 +283,16 @@ private async Task SendInvite() if (!DataService.DoesUserExist(_inviteEmail)) { var result = await DataService.CreateUser(_inviteEmail, _inviteAsAdmin, User.OrganizationID); - if (result) + if (result.IsSuccess) { - var user = await DataService.GetUserByName(_inviteEmail); + var userResult = await DataService.GetUserByName(_inviteEmail); + if (!userResult.IsSuccess) + { + ToastService.ShowToast2(userResult.Reason, Enums.ToastType.Warning); + return; + } + + var user = userResult.Value; await UserManager.ConfirmEmailAsync(user, await UserManager.GenerateEmailConfirmationTokenAsync(user)); diff --git a/Server/Pages/Shared/_Layout.cshtml b/Server/Pages/Shared/_Layout.cshtml index fc62ddbd6..535b01ae4 100644 --- a/Server/Pages/Shared/_Layout.cshtml +++ b/Server/Pages/Shared/_Layout.cshtml @@ -1,5 +1,6 @@ @using Microsoft.AspNetCore.Hosting @using Microsoft.AspNetCore.Mvc.ViewEngines +@using Microsoft.EntityFrameworkCore; @using Remotely.Server.Services @using Remotely.Shared.Models @inject IApplicationConfig AppConfig @@ -9,14 +10,18 @@ @{ var organizationName = "Remotely"; - var user = DataService.GetUserByNameWithOrg(User?.Identity?.Name); + var userResult = await DataService.GetUserByName( + $"{User?.Identity?.Name}", + query => query.Include(x => x.Organization)); + + var user = userResult.Value; if (user is null) { - var defaultOrg = await DataService.GetDefaultOrganization(); - if (!string.IsNullOrWhiteSpace(defaultOrg?.OrganizationName)) + var orgResult = await DataService.GetDefaultOrganization(); + if (orgResult.IsSuccess) { - organizationName = defaultOrg.OrganizationName; + organizationName = orgResult.Value.OrganizationName; } } else if (!string.IsNullOrWhiteSpace(user.Organization?.OrganizationName)) @@ -43,7 +48,7 @@ @if (user is RemotelyUser) { - switch (user.UserOptions.Theme) + switch (user.UserOptions?.Theme ?? default) { case Remotely.Shared.Enums.Theme.Light: diff --git a/Server/Pages/_Host.cshtml b/Server/Pages/_Host.cshtml index df5db9313..26289ed64 100644 --- a/Server/Pages/_Host.cshtml +++ b/Server/Pages/_Host.cshtml @@ -8,7 +8,8 @@ @{ Layout = null; - var user = DataService.GetUserByNameWithOrg(User?.Identity?.Name); + var userResult = await DataService.GetUserByName($"{User?.Identity?.Name}"); + var user = userResult.Value; } @@ -26,7 +27,7 @@ @if (user is RemotelyUser) { - switch (user.UserOptions.Theme) + switch (user.UserOptions?.Theme ?? default) { case Remotely.Shared.Enums.Theme.Light: diff --git a/Server/Services/CircuitManager.cs b/Server/Services/CircuitManager.cs index 48b9352df..ad44fa7b8 100644 --- a/Server/Services/CircuitManager.cs +++ b/Server/Services/CircuitManager.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; @@ -13,9 +14,9 @@ public interface ICircuitManager { ICollection Connections { get; } bool TryAddConnection(string id, ICircuitConnection connection); - bool TryRemoveConnection(string id, out ICircuitConnection connection); - Task InvokeOnConnection(string browserConnectionId, CircuitEventName eventName, params object[] args); - bool TryGetConnection(string browserConnectionId, out ICircuitConnection connection); + bool TryRemoveConnection(string id, [NotNullWhen(true)] out ICircuitConnection? connection); + Task InvokeOnConnection(string id, CircuitEventName eventName, params object[] args); + bool TryGetConnection(string id, [NotNullWhen(true)] out ICircuitConnection? connection); } public class CircuitManager : ICircuitManager { @@ -52,12 +53,12 @@ public bool TryAddConnection(string id, ICircuitConnection connection) return _connections.TryAdd(id, connection); } - public bool TryGetConnection(string connectionId, out ICircuitConnection connection) + public bool TryGetConnection(string id, [NotNullWhen(true)] out ICircuitConnection? connection) { - return _connections.TryGetValue(connectionId, out connection); + return _connections.TryGetValue(id, out connection); } - public bool TryRemoveConnection(string id, out ICircuitConnection connection) + public bool TryRemoveConnection(string id, [NotNullWhen(true)] out ICircuitConnection? connection) { return _connections.TryRemove(id, out connection); } diff --git a/Server/Services/ClientAppState.cs b/Server/Services/ClientAppState.cs index 859bd6df1..95d283d3b 100644 --- a/Server/Services/ClientAppState.cs +++ b/Server/Services/ClientAppState.cs @@ -89,9 +89,13 @@ public async Task GetEffectiveTheme() { if (await _authService.IsAuthenticated()) { - var user = await _authService.GetUser(); - return user?.UserOptions?.Theme ?? _appConfig.Theme; + var userResult = await _authService.GetUser(); + if (userResult.IsSuccess) + { + return userResult.Value.UserOptions?.Theme ?? _appConfig.Theme; + } } + return _appConfig.Theme; } diff --git a/Server/Services/DataService.cs b/Server/Services/DataService.cs index cfafa9988..bf6dc3842 100644 --- a/Server/Services/DataService.cs +++ b/Server/Services/DataService.cs @@ -214,7 +214,7 @@ Task UpdateBrandingInfo( Task> UpdateDevice(DeviceSetupOptions deviceOptions, string organizationId); - Task UpdateDevice(string deviceId, string tag, string alias, string deviceGroupId, string notes); + Task UpdateDevice(string deviceId, string? tag, string? alias, string? deviceGroupId, string? notes); Task UpdateOrganizationName(string orgId, string newName); @@ -1448,6 +1448,11 @@ public async Task> GetOrganizationById(string organizationI public async Task> GetOrganizationByUserName(string userName) { + if (string.IsNullOrWhiteSpace(userName)) + { + return Result.Fail("User name is required."); + } + using var dbContext = _appDbFactory.GetContext(); var user = await dbContext @@ -1493,6 +1498,11 @@ public async Task> GetOrganizationNameById(string organizationId) public async Task> GetOrganizationNameByUserName(string userName) { + if (string.IsNullOrWhiteSpace(userName)) + { + return Result.Fail("Username cannot be empty."); + } + using var dbContext = _appDbFactory.GetContext(); var user = await dbContext.Users @@ -1737,6 +1747,15 @@ public async Task> GetUserOptions(string userName) public async Task JoinViaInvitation(string userName, string inviteId) { + if (string.IsNullOrWhiteSpace(userName)) + { + return Result.Fail("Username cannot be empty."); + } + if (string.IsNullOrWhiteSpace(inviteId)) + { + return Result.Fail("Invite ID cannot be empty."); + } + using var dbContext = _appDbFactory.GetContext(); var invite = await dbContext.InviteLinks.FirstOrDefaultAsync(x => @@ -2014,7 +2033,7 @@ public async Task UpdateBrandingInfo( await dbContext.SaveChangesAsync(); } - public async Task UpdateDevice(string deviceId, string tag, string alias, string deviceGroupId, string notes) + public async Task UpdateDevice(string deviceId, string? tag, string? alias, string? deviceGroupId, string? notes) { using var dbContext = _appDbFactory.GetContext(); diff --git a/Server/Services/RcImplementations/ViewerPageDataProvider.cs b/Server/Services/RcImplementations/ViewerPageDataProvider.cs index d7472a0fc..31df87996 100644 --- a/Server/Services/RcImplementations/ViewerPageDataProvider.cs +++ b/Server/Services/RcImplementations/ViewerPageDataProvider.cs @@ -68,21 +68,22 @@ public Task GetTheme(PageModel pageModel) //return Task.FromResult(appTheme); } - public Task GetUserDisplayName(PageModel pageModel) + public async Task GetUserDisplayName(PageModel pageModel) { if (string.IsNullOrWhiteSpace(pageModel?.User?.Identity?.Name)) { - return Task.FromResult(string.Empty); + return string.Empty; } - var user = _dataService.GetUserByNameWithOrg(pageModel.User.Identity.Name); + var userResult = await _dataService.GetUserByName(pageModel.User.Identity.Name); - if (user is null) + if (!userResult.IsSuccess) { - return Task.FromResult(string.Empty); + return string.Empty; } + var user = userResult.Value; var displayName = user.UserOptions?.DisplayName ?? user.UserName ?? string.Empty; - return Task.FromResult(displayName); + return displayName; } } diff --git a/Shared/Models/Device.cs b/Shared/Models/Device.cs index 87aced45d..865aed44c 100644 --- a/Shared/Models/Device.cs +++ b/Shared/Models/Device.cs @@ -91,7 +91,7 @@ public class Device [StringLength(200)] [Sortable] [Display(Name = "Tags")] - public string Tags { get; set; } = ""; + public string? Tags { get; set; } [Sortable] [Display(Name = "Memory Total")] diff --git a/Tests/Server.Tests/DataServiceTests.cs b/Tests/Server.Tests/DataServiceTests.cs index 2b55839f3..afec5c7ce 100644 --- a/Tests/Server.Tests/DataServiceTests.cs +++ b/Tests/Server.Tests/DataServiceTests.cs @@ -16,8 +16,8 @@ namespace Remotely.Tests; public class DataServiceTests { private readonly string _newDeviceID = "NewDeviceName"; - private IDataService _dataService; - private TestData _testData; + private IDataService _dataService = null!; + private TestData _testData = null!; [TestMethod] public async Task AddAlert() @@ -32,7 +32,7 @@ public async Task AddAlert() [TestMethod] public async Task AddOrUpdateDevice() { - var storedDevice = _dataService.GetDevice(_newDeviceID); + var storedDevice = (await _dataService.GetDevice(_newDeviceID)).Value; Assert.IsNull(storedDevice); @@ -47,9 +47,9 @@ public async Task AddOrUpdateDevice() var result = await _dataService.AddOrUpdateDevice(newDevice); Assert.IsTrue(result.IsSuccess); - storedDevice = _dataService.GetDevice(_newDeviceID); + storedDevice = (await _dataService.GetDevice(_newDeviceID)).Value; - Assert.AreEqual(_newDeviceID, storedDevice.ID); + Assert.AreEqual(_newDeviceID, storedDevice!.ID); Assert.AreEqual(Environment.MachineName, storedDevice.DeviceName); Assert.AreEqual(Environment.Is64BitOperatingSystem, storedDevice.Is64Bit); } @@ -76,12 +76,12 @@ public async Task CreateDevice() [TestMethod] public void DeviceGroupPermissions() { - 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.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)); @@ -91,16 +91,16 @@ public void DeviceGroupPermissions() Assert.IsFalse(_dataService.DoesUserHaveAccessToDevice(_testData.Org1Device1.ID, _testData.Org2User2)); var groupID = _testData.Org1Group1.ID; - _dataService.AddUserToDeviceGroup(_testData.Org1Id, groupID, _testData.Org1User1.UserName, out _); + _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.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)); @@ -110,12 +110,12 @@ public void DeviceGroupPermissions() 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); + 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] @@ -198,12 +198,12 @@ public async Task TestInit() } [TestMethod] - public void UpdateOrganizationName() + public async Task UpdateOrganizationName() { - Assert.AreEqual("Org1", _testData.Org1Admin1.Organization.OrganizationName); - _dataService.UpdateOrganizationName(_testData.Org1Id, "Test Org"); - var updatedOrg = _dataService.GetOrganizationById(_testData.Org1Id); - Assert.AreEqual("Test Org", updatedOrg.OrganizationName); + Assert.AreEqual("Org1", _testData.Org1Admin1.Organization!.OrganizationName); + await _dataService.UpdateOrganizationName(_testData.Org1Id, "Test Org"); + var updatedOrg = (await _dataService.GetOrganizationById(_testData.Org1Id)).Value; + Assert.AreEqual("Test Org", updatedOrg!.OrganizationName); } [TestMethod] @@ -217,8 +217,8 @@ public async Task UpdateServerAdmins() currentAdmins = _dataService.GetServerAdmins(); Assert.AreEqual(2, currentAdmins.Count); - Assert.IsTrue(currentAdmins.Contains(_testData.Org1Admin1.UserName)); - Assert.IsTrue(currentAdmins.Contains(_testData.Org1Admin2.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.Org1Admin2.Id, false, _testData.Org1Admin2.Id); @@ -237,12 +237,12 @@ public async Task UpdateServerAdmins() } [TestMethod] - public void VerifyInitialData() + public async Task VerifyInitialData() { - 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.IsNotNull((await _dataService.GetUserByName(_testData.Org1Admin1.UserName!)).Value); + Assert.IsNotNull((await _dataService.GetUserByName(_testData.Org1Admin2.UserName!)).Value); + Assert.IsNotNull((await _dataService.GetUserByName(_testData.Org1User1.UserName!)).Value); + Assert.IsNotNull((await _dataService.GetUserByName(_testData.Org1User2.UserName!)).Value); Assert.AreEqual(2, _dataService.GetOrganizationCount()); var devices1 = _dataService.GetAllDevices(_testData.Org1Id); diff --git a/Tests/Server.Tests/TestData.cs b/Tests/Server.Tests/TestData.cs index 0800ef507..14089fa6b 100644 --- a/Tests/Server.Tests/TestData.cs +++ b/Tests/Server.Tests/TestData.cs @@ -17,7 +17,7 @@ namespace Remotely.Tests; public class TestData { #region Organization1 - public Organization Org1 => Org1Admin1.Organization; + public Organization Org1 => Org1Admin1.Organization!; public RemotelyUser Org1Admin1 { get; } = new() { @@ -28,11 +28,11 @@ public class TestData UserOptions = new RemotelyUserOptions() }; - public RemotelyUser Org1Admin2 { get; private set; } + public RemotelyUser Org1Admin2 { get; private set; } = null!; - public Device Org1Device1 { get; private set; } + public Device Org1Device1 { get; private set; } = null!; - public Device Org1Device2 { get; private set; } + public Device Org1Device2 { get; private set; } = null!; public DeviceGroup Org1Group1 { get; private set; } = new DeviceGroup() { @@ -45,14 +45,14 @@ public class TestData }; public string Org1Id => Org1.ID; - public RemotelyUser Org1User1 { get; private set; } - public RemotelyUser Org1User2 { get; private set; } + public RemotelyUser Org1User1 { get; private set; } = null!; + public RemotelyUser Org1User2 { get; private set; } = null!; #endregion #region Organization2 - public Organization Org2 => Org2Admin1.Organization; + public Organization Org2 => Org2Admin1.Organization!; public RemotelyUser Org2Admin1 { get; } = new() { @@ -63,11 +63,11 @@ public class TestData UserOptions = new RemotelyUserOptions() }; - public RemotelyUser Org2Admin2 { get; private set; } + public RemotelyUser Org2Admin2 { get; private set; } = null!; - public Device Org2Device1 { get; private set; } + public Device Org2Device1 { get; private set; } = null!; - public Device Org2Device2 { get; private set; } + public Device Org2Device2 { get; private set; } = null!; public DeviceGroup Org2Group1 { get; private set; } = new DeviceGroup() { @@ -80,8 +80,8 @@ public class TestData }; public string Org2Id => Org2.ID; - public RemotelyUser Org2User1 { get; private set; } - public RemotelyUser Org2User2 { get; private set; } + public RemotelyUser Org2User1 { get; private set; } = null!; + public RemotelyUser Org2User2 { get; private set; } = null!; #endregion public void ClearData() @@ -106,13 +106,13 @@ public async Task Init() await userManager.CreateAsync(Org1Admin1); await dataService.CreateUser("org1admin2@test.com", true, Org1Admin1.OrganizationID); - Org1Admin2 = dataService.GetUserByNameWithOrg("org1admin2@test.com"); + Org1Admin2 = (await dataService.GetUserByName("org1admin2@test.com")).Value!; await dataService.CreateUser("org1testuser1@test.com", false, Org1Admin1.OrganizationID); - Org1User1 = dataService.GetUserByNameWithOrg("org1testuser1@test.com"); + Org1User1 = (await dataService.GetUserByName("org1testuser1@test.com")).Value!; await dataService.CreateUser("org1testuser2@test.com", false, Org1Admin1.OrganizationID); - Org1User2 = dataService.GetUserByNameWithOrg("org1testuser2@test.com"); + Org1User2 = (await dataService.GetUserByName("org1testuser2@test.com")).Value!; var device1 = new DeviceClientDto() { @@ -126,12 +126,12 @@ public async Task Init() DeviceName = "Org1Device2Name", OrganizationID = Org1Id }; - Org1Device1 = (await dataService.AddOrUpdateDevice(device1)).Value; - Org1Device2 = (await dataService.AddOrUpdateDevice(device2)).Value; + Org1Device1 = (await dataService.AddOrUpdateDevice(device1)).Value!; + Org1Device2 = (await dataService.AddOrUpdateDevice(device2)).Value!; await dataService.AddDeviceGroup(Org1Admin1.OrganizationID, Org1Group1); await dataService.AddDeviceGroup(Org1Admin1.OrganizationID, Org1Group2); - var deviceGroups1 = dataService.GetDeviceGroups(Org1Admin1.UserName); + var deviceGroups1 = dataService.GetDeviceGroups(Org1Admin1.UserName!); Org1Group1 = deviceGroups1.First(x => x.Name == Org1Group1.Name); Org1Group2 = deviceGroups1.First(x => x.Name == Org1Group2.Name); @@ -140,13 +140,13 @@ public async Task Init() await userManager.CreateAsync(Org2Admin1); await dataService.CreateUser("org2admin2@test.com", true, Org2Admin1.OrganizationID); - Org2Admin2 = dataService.GetUserByNameWithOrg("org2admin2@test.com"); + Org2Admin2 = (await dataService.GetUserByName("org2admin2@test.com")).Value!; await dataService.CreateUser("org2testuser1@test.com", false, Org2Admin1.OrganizationID); - Org2User1 = dataService.GetUserByNameWithOrg("org2testuser1@test.com"); + Org2User1 = (await dataService.GetUserByName("org2testuser1@test.com")).Value!; await dataService.CreateUser("org2testuser2@test.com", false, Org2Admin1.OrganizationID); - Org2User2 = dataService.GetUserByNameWithOrg("org2testuser2@test.com"); + Org2User2 = (await dataService.GetUserByName("org2testuser2@test.com")).Value!; var device3 = new DeviceClientDto() { @@ -160,12 +160,12 @@ public async Task Init() DeviceName = "Org2Device2Name", OrganizationID = Org2Id }; - Org2Device1 = (await dataService.AddOrUpdateDevice(device3)).Value; - Org2Device2 = (await dataService.AddOrUpdateDevice(device4)).Value; + Org2Device1 = (await dataService.AddOrUpdateDevice(device3)).Value!; + Org2Device2 = (await dataService.AddOrUpdateDevice(device4)).Value!; await dataService.AddDeviceGroup(Org2Admin1.OrganizationID, Org2Group1); await dataService.AddDeviceGroup(Org2Admin1.OrganizationID, Org2Group2); - var deviceGroups2 = dataService.GetDeviceGroups(Org2Admin1.UserName); + var deviceGroups2 = dataService.GetDeviceGroups(Org2Admin1.UserName!); Org2Group1 = deviceGroups2.First(x => x.Name == Org2Group1.Name); Org2Group2 = deviceGroups2.First(x => x.Name == Org2Group2.Name); } From ac6487c6c5f101453190a51687f306f080572ed5 Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Wed, 26 Jul 2023 11:31:34 -0700 Subject: [PATCH 06/29] Another pass of nullable refactoring. --- .../Services/InstallerService.cs | 32 +++--- .../ViewModels/MainWindowViewModel.cs | 8 +- Agent/Models/ChatSession.cs | 2 +- Agent/Services/AgentHubConnection.cs | 108 +++++++++++++++--- Agent/Services/ChatClientService.cs | 6 +- Agent/Services/ConfigService.cs | 77 ++++++++----- Agent/Services/DeviceInfoGeneratorBase.cs | 7 +- Agent/Services/ExternalScriptingShell.cs | 38 +++--- Agent/Services/Linux/AppLauncherLinux.cs | 2 +- .../Linux/DeviceInfoGeneratorLinux.cs | 28 ++--- Agent/Services/Linux/UpdaterLinux.cs | 4 +- .../Services/MacOS/DeviceInfoGeneratorMac.cs | 2 +- Agent/Services/MacOS/UpdaterMac.cs | 4 +- Agent/Services/PsCoreShell.cs | 20 ++-- Agent/Services/ScriptExecutor.cs | 11 +- .../Windows/DeviceInfoGeneratorWin.cs | 2 +- Server/API/LoginController.cs | 2 +- .../API/OrganizationManagementController.cs | 2 +- Server/API/RemoteControlController.cs | 7 ++ .../Pages/Account/ConfirmEmail.cshtml.cs | 2 +- .../Account/ConfirmEmailChange.cshtml.cs | 2 +- .../Pages/Account/ForgotPassword.cshtml.cs | 6 +- .../Identity/Pages/Account/Login.cshtml.cs | 25 ++-- .../Identity/Pages/Account/Logout.cshtml | 2 +- .../Manage/EnableAuthenticator.cshtml.cs | 19 +-- .../Pages/Account/Manage/Index.cshtml.cs | 8 +- .../Pages/Account/Manage/ManageNavPages.cs | 2 +- Server/Auth/LocalOnlyFilter.cs | 2 +- Server/Auth/TwoFactorRequiredHandler.cs | 4 +- Server/Components/AlertBanner.razor | 4 +- Server/Components/AlertsFrame.razor | 5 +- Server/Components/ColorPicker.razor | 2 +- Server/Components/Devices/ChatCard.razor | 4 +- Server/Components/Devices/ChatCard.razor.cs | 23 ++-- Server/Components/Devices/ChatFrame.razor.cs | 8 +- Server/Components/Devices/DeviceCard.razor.cs | 35 +++--- Server/Components/Devices/Terminal.razor.cs | 41 ++++--- Server/Components/DropdownButton.razor | 14 +-- Server/Components/FileInputButton.razor | 6 +- .../ModalContents/EditDeviceGroup.razor | 8 +- Server/Components/ModalHarness.razor | 8 +- Server/Components/Scripts/SavedScripts.razor | 2 +- Server/Components/TabControl/TabContent.razor | 7 +- Server/Components/TabControl/TabControl.razor | 8 +- Server/Components/TabControl/TabHeader.razor | 14 +-- Server/Components/ToastHarness.razor | 2 +- Server/Components/TreeView/TreeView.razor.cs | 24 ++-- .../Components/TreeView/TreeViewItem.razor.cs | 22 ++-- Server/Hubs/AgentHub.cs | 21 ++-- Server/Hubs/CircuitConnection.cs | 2 +- Server/Models/ApiLogin.cs | 4 +- Server/Models/ModalButton.cs | 6 +- Server/Models/RemoteControlRequest.cs | 6 +- Server/Pages/Branding.razor | 8 +- Server/Pages/DeviceDetails.razor | 6 +- Server/Pages/Error.cshtml.cs | 9 +- Server/Pages/GetSupport.cshtml.cs | 4 +- Server/Pages/ScriptsPage.razor | 4 +- Server/Pages/ServerConfig.razor.cs | 98 ++++++++++------ Server/Pages/UserOptions.razor | 4 +- Server/Services/AgentHubSessionCache.cs | 17 +-- Server/Services/ApplicationConfig.cs | 16 +-- Server/Services/ClientAppState.cs | 4 +- Server/Services/DataService.cs | 2 +- Server/Services/EmailSender.cs | 13 ++- Server/Services/ModalService.cs | 25 ++-- Server/Services/OtpProvider.cs | 3 +- Server/Services/ScriptScheduler.cs | 19 +-- Server/Services/ToastService.cs | 16 +-- Server/Services/UpgradeService.cs | 18 ++- Shared/Models/ConnectionInfo.cs | 2 +- Shared/Models/Device.cs | 1 - Shared/Models/PwshCommandCompletion.cs | 8 +- Shared/Models/RemotelyUserOptions.cs | 2 +- Shared/Services/ProcessInvoker.cs | 4 +- Shared/Utilities/AppVersionHelper.cs | 2 +- Shared/Utilities/ConsoleHelper.cs | 4 +- Shared/Utilities/Logger.cs | 4 +- Shared/ViewModels/ChatHistoryItem.cs | 2 +- Shared/ViewModels/ChatSession.cs | 4 +- Shared/ViewModels/InviteViewModel.cs | 4 +- Shared/ViewModels/OrganizationUser.cs | 4 +- Shared/ViewModels/TerminalLineItem.cs | 6 +- Tests/LoadTester/CommandLineParser.cs | 20 +++- Tests/LoadTester/Program.cs | 18 +-- Tests/Server.Tests/AgentHubTests.cs | 20 ++-- Tests/Server.Tests/CircuitConnectionTests.cs | 10 +- Tests/Server.Tests/IoCActivator.cs | 12 +- .../ScriptScheduleDispatcherTests.cs | 20 ++-- 89 files changed, 665 insertions(+), 458 deletions(-) diff --git a/Agent.Installer.Win/Services/InstallerService.cs b/Agent.Installer.Win/Services/InstallerService.cs index 110587166..3ac20966f 100644 --- a/Agent.Installer.Win/Services/InstallerService.cs +++ b/Agent.Installer.Win/Services/InstallerService.cs @@ -1,4 +1,5 @@ -using IWshRuntimeLibrary; +#nullable enable +using IWshRuntimeLibrary; using Microsoft.VisualBasic.FileIO; using Microsoft.Win32; using Remotely.Agent.Installer.Win.Utilities; @@ -26,14 +27,14 @@ public class InstallerService private readonly string _platform = Environment.Is64BitOperatingSystem ? "x64" : "x86"; private readonly JavaScriptSerializer _serializer = new JavaScriptSerializer(); - public event EventHandler ProgressMessageChanged; - public event EventHandler ProgressValueChanged; + public event EventHandler? ProgressMessageChanged; + public event EventHandler? ProgressValueChanged; public async Task Install(string serverUrl, string organizationId, - string deviceGroup, - string deviceAlias, - string deviceUuid, + string? deviceGroup, + string? deviceAlias, + string? deviceUuid, bool createSupportShortcut) { try @@ -174,8 +175,8 @@ private void ClearInstallDirectory() private async Task CreateDeviceOnServer(string deviceUuid, string serverUrl, - string deviceGroup, - string deviceAlias, + string? deviceGroup, + string? deviceAlias, string organizationId) { try @@ -199,9 +200,10 @@ private async Task CreateDeviceOnServer(string deviceUuid, { await sw.WriteAsync(_serializer.Serialize(setupOptions)); } - using (var response = await wr.GetResponseAsync() as HttpWebResponse) - { - Logger.Write($"Create device response: {response.StatusCode}"); + using var response = await wr.GetResponseAsync(); + if (response is HttpWebResponse httpResponse) + { + Logger.Write($"Create device response: {httpResponse.StatusCode}"); } } } @@ -261,7 +263,7 @@ private async Task DownloadRemotelyAgent(string serverUrl) } else { - ProgressMessageChanged.Invoke(this, "Downloading Remotely agent."); + ProgressMessageChanged?.Invoke(this, "Downloading Remotely agent."); using (var client = new WebClient()) { client.DownloadProgressChanged += (sender, args) => @@ -273,7 +275,7 @@ private async Task DownloadRemotelyAgent(string serverUrl) } } - ProgressMessageChanged.Invoke(this, "Extracting Remotely files."); + ProgressMessageChanged?.Invoke(this, "Extracting Remotely files."); ProgressValueChanged?.Invoke(this, 0); var tempDir = Path.Combine(Path.GetTempPath(), "RemotelyUpdate"); @@ -321,7 +323,7 @@ private async Task DownloadRemotelyAgent(string serverUrl) ProgressValueChanged?.Invoke(this, 0); } - private ConnectionInfo GetConnectionInfo(string organizationId, string serverUrl, string deviceUuid) + private ConnectionInfo GetConnectionInfo(string organizationId, string serverUrl, string? deviceUuid) { ConnectionInfo connectionInfo; var connectionInfoPath = Path.Combine(_installPath, "ConnectionInfo.json"); @@ -345,7 +347,7 @@ private ConnectionInfo GetConnectionInfo(string organizationId, string serverUrl { connectionInfo.ServerVerificationToken = null; } - connectionInfo.DeviceID = deviceUuid; + connectionInfo.DeviceID = deviceUuid!; } connectionInfo.OrganizationID = organizationId; connectionInfo.Host = serverUrl; diff --git a/Agent.Installer.Win/ViewModels/MainWindowViewModel.cs b/Agent.Installer.Win/ViewModels/MainWindowViewModel.cs index 94141a9c2..5b04f78d4 100644 --- a/Agent.Installer.Win/ViewModels/MainWindowViewModel.cs +++ b/Agent.Installer.Win/ViewModels/MainWindowViewModel.cs @@ -465,7 +465,13 @@ private async Task Install() HeaderMessage = "Installing Remotely..."; - if (await _installer.Install(ServerUrl, OrganizationID, DeviceGroup, DeviceAlias, DeviceUuid, CreateSupportShortcut)) + if (await _installer.Install( + ServerUrl, + OrganizationID!, + DeviceGroup, + DeviceAlias, + DeviceUuid, + CreateSupportShortcut)) { IsServiceInstalled = true; Progress = 0; diff --git a/Agent/Models/ChatSession.cs b/Agent/Models/ChatSession.cs index 7178d7c28..9743a1e76 100644 --- a/Agent/Models/ChatSession.cs +++ b/Agent/Models/ChatSession.cs @@ -5,5 +5,5 @@ namespace Remotely.Agent.Models; public class ChatSession { public int ProcessID { get; set; } - public NamedPipeClientStream PipeStream { get; set; } + public NamedPipeClientStream? PipeStream { get; set; } } diff --git a/Agent/Services/AgentHubConnection.cs b/Agent/Services/AgentHubConnection.cs index 97aa999fe..40f102696 100644 --- a/Agent/Services/AgentHubConnection.cs +++ b/Agent/Services/AgentHubConnection.cs @@ -10,12 +10,15 @@ using Remotely.Shared.Services; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net.Http; using System.Text; +using System.Threading; using System.Threading.Tasks; using System.Timers; +using Timer = System.Timers.Timer; namespace Remotely.Agent.Services; @@ -36,15 +39,16 @@ public class AgentHubConnection : IAgentHubConnection, IDisposable private readonly IHttpClientFactory _httpFactory; private readonly IWakeOnLanService _wakeOnLanService; private readonly ILogger _logger; - private readonly ILogger _fileLogger; + private readonly IEnumerable _loggerProviders; private readonly IScriptExecutor _scriptExecutor; private readonly IUninstaller _uninstaller; private readonly IUpdater _updater; - private ConnectionInfo _connectionInfo; - private HubConnection _hubConnection; - private Timer _heartbeatTimer; + private ConnectionInfo? _connectionInfo; + private HubConnection? _hubConnection; + private Timer? _heartbeatTimer; private bool _isServerVerified; + private FileLogger? _fileLogger; public AgentHubConnection( IConfigService configService, @@ -69,24 +73,41 @@ public AgentHubConnection( _httpFactory = httpFactory; _wakeOnLanService = wakeOnLanService; _logger = logger; - _fileLogger = loggerProviders - .OfType() - .FirstOrDefault() - ?.CreateLogger(nameof(AgentHubConnection)); + _loggerProviders = loggerProviders; } public bool IsConnected => _hubConnection?.State == HubConnectionState.Connected; public async Task Connect() { + using var throttle = new SemaphoreSlim(1, 1); + var count = 1; + while (true) { try { + var waitSeconds = Math.Min(60, Math.Pow(count, 2)); + // This will allow the first attempt to go through immediately, but + // subsequent attempts will have an exponential delay. + _ = await throttle.WaitAsync(TimeSpan.FromSeconds(waitSeconds)); + _logger.LogInformation("Attempting to connect to server."); _connectionInfo = _configService.GetConnectionInfo(); + if (string.IsNullOrWhiteSpace(_connectionInfo.OrganizationID)) + { + _logger.LogError("Organization ID is not set. Please set it in the config file."); + continue; + } + + if (string.IsNullOrWhiteSpace(_connectionInfo.Host)) + { + _logger.LogError("Host (server URL) is not set. Please set it in the config file."); + continue; + } + if (_hubConnection is not null) { await _hubConnection.DisposeAsync(); @@ -116,13 +137,11 @@ public async Task Connect() // The above can be caused by temporary issues on the server. So we'll do // nothing here and wait for it to get resolved. _logger.LogError("There was an issue registering with the server. The server might be undergoing maintenance, or the supplied organization ID might be incorrect."); - await Task.Delay(TimeSpan.FromMinutes(1)); continue; } if (!await VerifyServer()) { - await Task.Delay(TimeSpan.FromMinutes(1)); continue; } @@ -144,10 +163,7 @@ public async Task Connect() catch (Exception ex) { _logger.LogError(ex, "Error while connecting to server."); - await Task.Delay(5_000); } - - } } @@ -161,6 +177,17 @@ public async Task SendHeartbeat() { try { + if (_connectionInfo is null || _hubConnection is null) + { + return; + } + + if (string.IsNullOrWhiteSpace(_connectionInfo.OrganizationID)) + { + _logger.LogError("Organization ID is not set. Please set it in the config file."); + return; + } + var currentInfo = await _deviceInfoService.CreateDevice(_connectionInfo.DeviceID, _connectionInfo.OrganizationID); await _hubConnection.SendAsync("DeviceHeartbeat", currentInfo); } @@ -172,6 +199,11 @@ public async Task SendHeartbeat() private async Task CheckForServerMigration() { + if (_connectionInfo is null || _hubConnection is null) + { + return false; + } + var serverUrl = await _hubConnection.InvokeAsync("GetServerUrl"); if (Uri.TryCreate(serverUrl, UriKind.Absolute, out var serverUri) && @@ -187,17 +219,22 @@ private async Task CheckForServerMigration() return false; } - private async void HeartbeatTimer_Elapsed(object sender, ElapsedEventArgs e) + private async void HeartbeatTimer_Elapsed(object? sender, ElapsedEventArgs e) { await SendHeartbeat(); } - private async Task HubConnection_Reconnected(string arg) + private async Task HubConnection_Reconnected(string? arg) { + if (_connectionInfo is null || _hubConnection is null) + { + return; + } + _logger.LogInformation("Reconnected to server."); await _updater.CheckForUpdates(); - var device = await _deviceInfoService.CreateDevice(_connectionInfo.DeviceID, _connectionInfo.OrganizationID); + var device = await _deviceInfoService.CreateDevice(_connectionInfo.DeviceID, $"{_connectionInfo.OrganizationID}"); if (!await _hubConnection.InvokeAsync("DeviceCameOnline", device)) { @@ -214,6 +251,10 @@ private async Task HubConnection_Reconnected(string arg) private void RegisterMessageHandlers() { + if (_hubConnection is null) + { + throw new InvalidOperationException("Hub connection is null."); + } _hubConnection.On("ChangeWindowsSession", async (string viewerConnectionId, string sessionId, string accessKey, string userConnectionId, string requesterName, string orgName, string orgId, int targetSessionID) => { @@ -263,6 +304,10 @@ private void RegisterMessageHandlers() _hubConnection.On("DeleteLogs", () => { + if (TryGetFileLogger(out var fileLogger)) + { + fileLogger.DeleteLogs(); + } if (_fileLogger is FileLogger logger) { logger.DeleteLogs(); @@ -466,13 +511,14 @@ private void RegisterMessageHandlers() foreach (var fileID in fileIDs) { - var url = $"{_connectionInfo.Host}/API/FileSharing/{fileID}"; + var url = $"{_connectionInfo?.Host}/API/FileSharing/{fileID}"; using var client = _httpFactory.CreateClient(); client.DefaultRequestHeaders.Add(AppConstants.ExpiringTokenHeaderName, expiringToken); using var response = await client.GetAsync(url); - var filename = response.Content.Headers.ContentDisposition.FileName; - var legalChars = filename.ToCharArray().Where(x => !Path.GetInvalidFileNameChars().Any(y => x == y)); + var filename = response.Content.Headers.ContentDisposition?.FileName ?? Path.GetRandomFileName(); + var invalidChars = Path.GetInvalidFileNameChars().ToHashSet(); + var legalChars = filename.ToCharArray().Where(x => !invalidChars.Contains(x)); filename = new string(legalChars.ToArray()); @@ -499,8 +545,32 @@ private void RegisterMessageHandlers() }); } + private bool TryGetFileLogger([NotNullWhen(true)] out FileLogger? fileLogger) + { + if (_fileLogger is null) + { + var logger = _loggerProviders + .OfType() + .FirstOrDefault() + ?.CreateLogger(nameof(AgentHubConnection)); + + if (logger is FileLogger loggerImpl) + { + _fileLogger = loggerImpl; + } + } + + fileLogger = _fileLogger; + return fileLogger is not null; + } + private async Task VerifyServer() { + if (_connectionInfo is null || _hubConnection is null) + { + return false; + } + if (string.IsNullOrWhiteSpace(_connectionInfo.ServerVerificationToken)) { _isServerVerified = true; diff --git a/Agent/Services/ChatClientService.cs b/Agent/Services/ChatClientService.cs index 864bbe359..a6dfc65ce 100644 --- a/Agent/Services/ChatClientService.cs +++ b/Agent/Services/ChatClientService.cs @@ -39,7 +39,7 @@ public class ChatClientService : IChatClientService { return; } - chatSession.PipeStream.Dispose(); + chatSession.PipeStream?.Dispose(); var chatProcess = Process.GetProcessById(chatSession.ProcessID); if (chatProcess?.HasExited == false) { @@ -111,7 +111,7 @@ public async Task SendMessage( chatSession = (ChatSession)_chatClients.Get(senderConnectionID); - if (!chatSession.PipeStream.IsConnected) + if (chatSession.PipeStream?.IsConnected != true) { _chatClients.Remove(senderConnectionID); await hubConnection.SendAsync("DisplayMessage", "Chat disconnected. Please try again.", "Chat disconnected.", "bg-warning", senderConnectionID); @@ -142,7 +142,7 @@ private async Task ReadFromStream(NamedPipeClientStream clientPipe, string sende if (!string.IsNullOrWhiteSpace(messageJson)) { var chatMessage = JsonSerializer.Deserialize(messageJson); - await hubConnection.SendAsync("Chat", chatMessage.Message, false, senderConnectionID); + await hubConnection.SendAsync("Chat", $"{chatMessage?.Message}", false, senderConnectionID); } } await hubConnection.SendAsync("Chat", string.Empty, true, senderConnectionID); diff --git a/Agent/Services/ConfigService.cs b/Agent/Services/ConfigService.cs index fe8c80564..6eaea12c6 100644 --- a/Agent/Services/ConfigService.cs +++ b/Agent/Services/ConfigService.cs @@ -1,10 +1,12 @@ -using Microsoft.Extensions.Logging; +using Immense.RemoteControl.Shared; +using Microsoft.Extensions.Logging; 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.Text.Json; namespace Remotely.Agent.Services; @@ -18,16 +20,17 @@ public interface IConfigService public class ConfigService : IConfigService { private static readonly object _fileLock = new(); - private ConnectionInfo _connectionInfo; private readonly string _debugGuid = "f2b0a595-5ea8-471b-975f-12e70e0f3497"; private readonly ILogger _logger; + private ConnectionInfo? _connectionInfo; public ConfigService(ILogger logger) { _logger = logger; } - private Dictionary _commandLineArgs; + private Dictionary? _commandLineArgs; + private Dictionary CommandLineArgs { get @@ -36,13 +39,14 @@ private Dictionary CommandLineArgs { _commandLineArgs = new Dictionary(); var args = Environment.GetCommandLineArgs(); + for (var i = 1; i < args.Length; i += 2) { - var key = args?[i]; + var key = args[i]; if (key != null) { key = key.Trim().Replace("-", "").ToLower(); - var value = args?[i + 1]; + var value = args[i + 1]; if (value != null) { _commandLineArgs[key] = args[i + 1].Trim(); @@ -57,42 +61,53 @@ private Dictionary CommandLineArgs public ConnectionInfo GetConnectionInfo() { - // For debugging purposes (i.e. launch of a bunch of instances). - if (CommandLineArgs.TryGetValue("organization", out var orgID) && - CommandLineArgs.TryGetValue("host", out var hostName) && - CommandLineArgs.TryGetValue("device", out var deviceID)) - { - return new ConnectionInfo() + try + { // For debugging purposes (i.e. launch of a bunch of instances). + if (CommandLineArgs.TryGetValue("organization", out var orgID) && + CommandLineArgs.TryGetValue("host", out var hostName) && + CommandLineArgs.TryGetValue("device", out var deviceID)) { - DeviceID = deviceID, - Host = hostName, - OrganizationID = orgID - }; - } + return new ConnectionInfo() + { + DeviceID = deviceID, + Host = hostName, + OrganizationID = orgID + }; + } - if (Environment.UserInteractive && Debugger.IsAttached) - { - return new ConnectionInfo() + if (Environment.UserInteractive && Debugger.IsAttached) { - DeviceID = _debugGuid, - Host = "http://localhost:5000", - OrganizationID = orgID - }; - } + return new ConnectionInfo() + { + DeviceID = _debugGuid, + Host = "http://localhost:5000", + OrganizationID = orgID + }; + } - if (_connectionInfo == null) - { - lock (_fileLock) + if (_connectionInfo == null) { - if (!File.Exists("ConnectionInfo.json")) + lock (_fileLock) { - _logger.LogError("No connection info available. Please create ConnectionInfo.json file with appropriate values."); - return null; + if (!File.Exists("ConnectionInfo.json")) + { + _logger.LogError("No connection info available. Please create ConnectionInfo.json file with appropriate values."); + throw new InvalidOperationException("Config file does not exist."); + } + _connectionInfo = JsonSerializer.Deserialize(File.ReadAllText("ConnectionInfo.json")); } - _connectionInfo = JsonSerializer.Deserialize(File.ReadAllText("ConnectionInfo.json")); } } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to retrieve connection info."); + } + + if (_connectionInfo is null) + { + throw new InvalidOperationException("Unable to load config data."); + } return _connectionInfo; } diff --git a/Agent/Services/DeviceInfoGeneratorBase.cs b/Agent/Services/DeviceInfoGeneratorBase.cs index 595dbfa5d..937e31666 100644 --- a/Agent/Services/DeviceInfoGeneratorBase.cs +++ b/Agent/Services/DeviceInfoGeneratorBase.cs @@ -45,14 +45,15 @@ protected DeviceClientDto GetDeviceBase(string deviceID, string orgID) { try { - DriveInfo systemDrive; + DriveInfo? systemDrive; var allDrives = DriveInfo.GetDrives(); if (EnvironmentHelper.IsWindows) { + var rootPath = Path.GetPathRoot(Environment.SystemDirectory) ?? "C:\\"; systemDrive = allDrives.FirstOrDefault(x => x.IsReady && - x.RootDirectory.FullName.Contains(Path.GetPathRoot(Environment.SystemDirectory ?? Environment.CurrentDirectory))); + x.RootDirectory.FullName.Contains(rootPath)); } else { @@ -97,7 +98,7 @@ protected List GetAllDrives() catch (Exception ex) { _logger.LogError(ex, "Error getting drive info."); - return null; + return new(); } } diff --git a/Agent/Services/ExternalScriptingShell.cs b/Agent/Services/ExternalScriptingShell.cs index 45f1a7a3e..37f7a58fa 100644 --- a/Agent/Services/ExternalScriptingShell.cs +++ b/Agent/Services/ExternalScriptingShell.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; +using Immense.RemoteControl.Shared.Extensions; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Remotely.Shared.Enums; using Remotely.Shared.Models; @@ -8,12 +9,13 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using System.Timers; namespace Remotely.Agent.Services; public interface IExternalScriptingShell { - Process ShellProcess { get; } + Process? ShellProcess { get; } Task Init(ScriptingShell shell, string shellProcessName, string lineEnding, string connectionId); Task WriteInput(string input, TimeSpan timeout); } @@ -27,13 +29,13 @@ public class ExternalScriptingShell : IExternalScriptingShell private readonly SemaphoreSlim _writeLock = new(1, 1); private string _errorOut = string.Empty; private string _lastInputID = string.Empty; - private string _lineEnding; + private string _lineEnding = Environment.NewLine; private System.Timers.Timer _processIdleTimeout = new(TimeSpan.FromMinutes(10)) { AutoReset = false }; - private string _senderConnectionId; + private string? _senderConnectionId; private ScriptingShell _shell; private string _standardOut = string.Empty; @@ -44,7 +46,8 @@ public ExternalScriptingShell( _configService = configService; _logger = logger; } - public Process ShellProcess { get; set; } + + public Process? ShellProcess { get; private set; } // TODO: Turn into cache and factory. @@ -94,9 +97,9 @@ public async Task Init(ScriptingShell shell, string shellProcessName, string lin RedirectStandardOutput = true }; - var connectionInfo = _configService.GetConnectionInfo(); - psi.Environment.Add("DeviceId", connectionInfo.DeviceID); - psi.Environment.Add("ServerUrl", connectionInfo.Host); + var configInfo = _configService.GetConnectionInfo(); + psi.Environment.Add("DeviceId", configInfo.DeviceID); + psi.Environment.Add("ServerUrl", configInfo.Host); ShellProcess = new Process { @@ -133,6 +136,11 @@ public async Task WriteInput(string input, TimeSpan timeout) try { + if (ShellProcess?.HasExited != false) + { + throw new InvalidOperationException("Shell process is not running."); + } + _processIdleTimeout.Stop(); _processIdleTimeout.Start(); _outputDone.Reset(); @@ -190,7 +198,7 @@ private ScriptResult GenerateCompletedResult(string input, TimeSpan runtime) StandardOutput = _standardOut.Split(Environment.NewLine), ErrorOutput = _errorOut.Split(Environment.NewLine), HadErrors = !string.IsNullOrWhiteSpace(_errorOut) || - (ShellProcess.HasExited && ShellProcess.ExitCode != 0) + (ShellProcess?.HasExited == true && ShellProcess.ExitCode != 0) }; } @@ -210,12 +218,12 @@ private ScriptResult GeneratePartialResult(string input, TimeSpan runtime) .Concat(_errorOut.Split(Environment.NewLine)) .ToArray(), HadErrors = !string.IsNullOrWhiteSpace(_errorOut) || - (ShellProcess.HasExited && ShellProcess.ExitCode != 0) + (ShellProcess?.HasExited == true && ShellProcess.ExitCode != 0) }; - ProcessIdleTimeout_Elapsed(this, null); + RemoveSession(); return partialResult; } - private void ProcessIdleTimeout_Elapsed(object sender, System.Timers.ElapsedEventArgs e) + private void ProcessIdleTimeout_Elapsed(object? sender, ElapsedEventArgs e) { RemoveSession(); } @@ -223,6 +231,10 @@ private void ProcessIdleTimeout_Elapsed(object sender, System.Timers.ElapsedEven private void RemoveSession() { ShellProcess?.Kill(); + if (_senderConnectionId is null) + { + return; + } _sessions.TryRemove(_senderConnectionId, out _); } @@ -236,7 +248,7 @@ private void ShellProcess_ErrorDataReceived(object sender, DataReceivedEventArgs private void ShellProcess_OutputDataReceived(object sender, DataReceivedEventArgs e) { - if (e?.Data?.Contains(_lastInputID) == true) + if (e.Data?.Contains(_lastInputID) == true) { _outputDone.Set(); } diff --git a/Agent/Services/Linux/AppLauncherLinux.cs b/Agent/Services/Linux/AppLauncherLinux.cs index 6eb14e53f..53e3106c7 100644 --- a/Agent/Services/Linux/AppLauncherLinux.cs +++ b/Agent/Services/Linux/AppLauncherLinux.cs @@ -76,7 +76,7 @@ private int StartLinuxDesktopApp(string args) xauthority, display, args); - return Process.Start(psi).Id; + return Process.Start(psi)?.Id ?? throw new InvalidOperationException("Failed to launch desktop app."); } private string GetXorgAuth() diff --git a/Agent/Services/Linux/DeviceInfoGeneratorLinux.cs b/Agent/Services/Linux/DeviceInfoGeneratorLinux.cs index eedce258b..2a8e13232 100644 --- a/Agent/Services/Linux/DeviceInfoGeneratorLinux.cs +++ b/Agent/Services/Linux/DeviceInfoGeneratorLinux.cs @@ -56,7 +56,7 @@ public Task CreateDevice(string deviceId, string orgId) private string GetCurrentUser() { var users = _processInvoker.InvokeProcessOutput("users", ""); - return users?.Split()?.FirstOrDefault()?.Trim(); + return users?.Split()?.FirstOrDefault()?.Trim() ?? string.Empty; } public (double usedGB, double totalGB) GetMemoryInGB() @@ -67,21 +67,23 @@ private string GetCurrentUser() var resultsArr = results.Split("\n".ToCharArray()); var freeKB = resultsArr .FirstOrDefault(x => x.Trim().StartsWith("MemAvailable")) - .Trim() - .Split(" ".ToCharArray(), 2) - .Last() // 9168236 kB - .Trim() - .Split(' ') - .First(); // 9168236 + ?.Trim() + ?.Split(" ".ToCharArray(), 2) + ?.Last() // 9168236 kB + ?.Trim() + ?.Split(' ') + ?.First() // 9168236 + ?? "0"; var totalKB = resultsArr .FirstOrDefault(x => x.Trim().StartsWith("MemTotal")) - .Trim() - .Split(" ".ToCharArray(), 2) - .Last() // 16637468 kB - .Trim() - .Split(' ') - .First(); // 16637468 + ?.Trim() + ?.Split(" ".ToCharArray(), 2) + ?.Last() // 16637468 kB + ?.Trim() + ?.Split(' ') + ?.First() // 16637468 + ?? "0"; var freeGB = Math.Round(double.Parse(freeKB) / 1024 / 1024, 2); var totalGB = Math.Round(double.Parse(totalKB) / 1024 / 1024, 2); diff --git a/Agent/Services/Linux/UpdaterLinux.cs b/Agent/Services/Linux/UpdaterLinux.cs index 141c0e4cd..88262c949 100644 --- a/Agent/Services/Linux/UpdaterLinux.cs +++ b/Agent/Services/Linux/UpdaterLinux.cs @@ -105,7 +105,7 @@ public async Task CheckForUpdates() await InstallLatestVersion(); } - catch (WebException ex) when ((ex.Response as HttpWebResponse).StatusCode == HttpStatusCode.NotModified) + catch (WebException ex) when (ex.Response is HttpWebResponse http && http.StatusCode == HttpStatusCode.NotModified) { _logger.LogInformation("Service Updater: Version is current."); return; @@ -180,7 +180,7 @@ await _updateDownloader.DownloadFile( } } - private async void UpdateTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) + private async void UpdateTimer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e) { await CheckForUpdates(); } diff --git a/Agent/Services/MacOS/DeviceInfoGeneratorMac.cs b/Agent/Services/MacOS/DeviceInfoGeneratorMac.cs index cfa646f87..23f083750 100644 --- a/Agent/Services/MacOS/DeviceInfoGeneratorMac.cs +++ b/Agent/Services/MacOS/DeviceInfoGeneratorMac.cs @@ -114,6 +114,6 @@ private Task GetCpuUtilization() private string GetCurrentUser() { var users = _processInvoker.InvokeProcessOutput("users", ""); - return users?.Split()?.FirstOrDefault()?.Trim(); + return users?.Split()?.FirstOrDefault()?.Trim() ?? string.Empty; } } diff --git a/Agent/Services/MacOS/UpdaterMac.cs b/Agent/Services/MacOS/UpdaterMac.cs index 5e9641bf6..a131efb26 100644 --- a/Agent/Services/MacOS/UpdaterMac.cs +++ b/Agent/Services/MacOS/UpdaterMac.cs @@ -106,7 +106,7 @@ public async Task CheckForUpdates() await InstallLatestVersion(); } - catch (WebException ex) when ((ex.Response as HttpWebResponse).StatusCode == HttpStatusCode.NotModified) + catch (WebException ex) when (ex.Response is HttpWebResponse http && http.StatusCode == HttpStatusCode.NotModified) { _logger.LogInformation("Service Updater: Version is current."); return; @@ -166,7 +166,7 @@ await _updateDownloader.DownloadFile( } } - private async void UpdateTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) + private async void UpdateTimer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e) { await CheckForUpdates(); } diff --git a/Agent/Services/PsCoreShell.cs b/Agent/Services/PsCoreShell.cs index e3044798f..d115e50f4 100644 --- a/Agent/Services/PsCoreShell.cs +++ b/Agent/Services/PsCoreShell.cs @@ -12,7 +12,7 @@ namespace Remotely.Agent.Services; public interface IPsCoreShell { - string SenderConnectionId { get; set; } + string? SenderConnectionId { get; set; } CommandCompletion GetCompletions(string inputText, int currentIndex, bool? forward); Task WriteInput(string input); @@ -25,15 +25,16 @@ public class PsCoreShell : IPsCoreShell private readonly ConnectionInfo _connectionInfo; private readonly ILogger _logger; private readonly PowerShell _powershell; - private CommandCompletion _lastCompletion; - private string _lastInputText; + private CommandCompletion? _lastCompletion; + private string? _lastInputText; + public PsCoreShell( IConfigService configService, ILogger logger) { _configService = configService; - _logger = logger; _connectionInfo = _configService.GetConnectionInfo(); + _logger = logger; _powershell = PowerShell.Create(); @@ -47,7 +48,8 @@ public PsCoreShell( _powershell.Invoke(); } - public string SenderConnectionId { get; set; } + public string? SenderConnectionId { get; set; } + // TODO: Turn into cache and factory. public static IPsCoreShell GetCurrent(string senderConnectionId) { @@ -99,7 +101,10 @@ public async Task WriteInput(string input) ps.AddScript("$args[0] | Out-String"); ps.AddArgument(results); var result = await ps.InvokeAsync(); - var hostOutput = result[0].BaseObject.ToString(); + + var hostOutput = result.Count > 0 ? + $"{result[0].BaseObject}" : + string.Empty; var verboseOut = _powershell.Streams.Verbose.ReadAll().Select(x => x.Message); var debugOut = _powershell.Streams.Debug.ReadAll().Select(x => x.Message); @@ -110,7 +115,8 @@ public async Task WriteInput(string input) var standardOut = hostOutput.Split(Environment.NewLine) .Concat(infoOut) .Concat(debugOut) - .Concat(verboseOut); + .Concat(verboseOut) + .Select(x => $"{x}"); var errorAndWarningOut = errorOut.Concat(warningOut).ToArray(); diff --git a/Agent/Services/ScriptExecutor.cs b/Agent/Services/ScriptExecutor.cs index 71ca180d7..1e983fd8c 100644 --- a/Agent/Services/ScriptExecutor.cs +++ b/Agent/Services/ScriptExecutor.cs @@ -1,4 +1,6 @@ -using Microsoft.AspNetCore.SignalR.Client; +using Immense.RemoteControl.Shared.Extensions; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Remotely.Shared; using Remotely.Shared.Enums; @@ -103,8 +105,7 @@ public async Task RunScript(Guid savedScriptId, scriptRunId, initiator); - var connectionInfo = _configService.GetConnectionInfo(); - var url = $"{connectionInfo.Host}/API/SavedScripts/{savedScriptId}"; + var url = $"{_configService.GetConnectionInfo().Host}/API/SavedScripts/{savedScriptId}"; using var hc = new HttpClient(); hc.DefaultRequestHeaders.Add(AppConstants.ExpiringTokenHeaderName, expiringToken); var response = await hc.GetAsync(url); @@ -193,9 +194,9 @@ private async Task ExecuteScriptContent( default: break; } - return null; + throw new InvalidOperationException($"Unknown shell type: {shell}"); } - private async Task SendResultsToApi(object result, string expiringToken) + private async Task SendResultsToApi(object result, string expiringToken) { var targetURL = _configService.GetConnectionInfo().Host + $"/API/ScriptResults"; diff --git a/Agent/Services/Windows/DeviceInfoGeneratorWin.cs b/Agent/Services/Windows/DeviceInfoGeneratorWin.cs index 32ae5e029..65b4ecdff 100644 --- a/Agent/Services/Windows/DeviceInfoGeneratorWin.cs +++ b/Agent/Services/Windows/DeviceInfoGeneratorWin.cs @@ -30,7 +30,7 @@ public Task CreateDevice(string deviceId, string orgId) var (usedStorage, totalStorage) = GetSystemDriveInfo(); var (usedMemory, totalMemory) = GetMemoryInGB(); - device.CurrentUser = Win32Interop.GetActiveSessions().LastOrDefault()?.Username; + device.CurrentUser = Win32Interop.GetActiveSessions().LastOrDefault()?.Username ?? string.Empty; device.Drives = GetAllDrives(); device.UsedStorage = usedStorage; device.TotalStorage = totalStorage; diff --git a/Server/API/LoginController.cs b/Server/API/LoginController.cs index ddbee3b5f..fa8f3cf23 100644 --- a/Server/API/LoginController.cs +++ b/Server/API/LoginController.cs @@ -81,7 +81,7 @@ public async Task Post([FromBody] ApiLogin login) return NotFound(); } - var result = await _signInManager.PasswordSignInAsync(login.Email, login.Password, false, true); + var result = await _signInManager.PasswordSignInAsync($"{login.Email}", $"{login.Password}", false, true); if (result.Succeeded) { _logger.LogInformation("API login successful for {loginEmail}.", login.Email); diff --git a/Server/API/OrganizationManagementController.cs b/Server/API/OrganizationManagementController.cs index 5381190ef..323f1f601 100644 --- a/Server/API/OrganizationManagementController.cs +++ b/Server/API/OrganizationManagementController.cs @@ -288,7 +288,7 @@ public async Task SendInvite([FromBody] InviteViewModel invite) return Unauthorized(); } - if (!ModelState.IsValid) + if (!ModelState.IsValid || string.IsNullOrWhiteSpace(invite.InvitedUser)) { return BadRequest(); } diff --git a/Server/API/RemoteControlController.cs b/Server/API/RemoteControlController.cs index a20be9cff..aba579bdb 100644 --- a/Server/API/RemoteControlController.cs +++ b/Server/API/RemoteControlController.cs @@ -75,6 +75,13 @@ public async Task Post([FromBody] RemoteControlRequest rcRequest) return NotFound(); } + if (string.IsNullOrWhiteSpace(rcRequest.Email) || + string.IsNullOrWhiteSpace(rcRequest.Password) || + string.IsNullOrWhiteSpace(rcRequest.DeviceID)) + { + return BadRequest("Request body is missing required values."); + } + var userResult = await _dataService.GetUserByName(rcRequest.Email); if (!userResult.IsSuccess) { diff --git a/Server/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs b/Server/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs index ed2436b8a..46058053f 100644 --- a/Server/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs +++ b/Server/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs @@ -23,7 +23,7 @@ public ConfirmEmailModel(UserManager userManager) } [TempData] - public string StatusMessage { get; set; } + public string? StatusMessage { get; set; } public async Task OnGetAsync(string userId, string code) { diff --git a/Server/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml.cs b/Server/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml.cs index c1b7b2985..983f20391 100644 --- a/Server/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml.cs +++ b/Server/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml.cs @@ -25,7 +25,7 @@ public ConfirmEmailChangeModel(UserManager userManager, SignInMana } [TempData] - public string StatusMessage { get; set; } + public string? StatusMessage { get; set; } public async Task OnGetAsync(string userId, string email, string code) { diff --git a/Server/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs b/Server/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs index 3a33dce38..a4fb87fc4 100644 --- a/Server/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs +++ b/Server/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs @@ -37,13 +37,13 @@ public ForgotPasswordModel( } [BindProperty] - public InputModel Input { get; set; } + public InputModel Input { get; set; } = new(); public class InputModel { [Required] [EmailAddress] - public string Email { get; set; } + public string Email { get; set; } = string.Empty; } public async Task OnPostAsync() @@ -65,7 +65,7 @@ public async Task OnPostAsync() "/Account/ResetPassword", pageHandler: null, values: new { area = "Identity", code }, - protocol: Request.Scheme); + protocol: Request.Scheme)!; _logger.LogInformation( "Sending password reset for user {username}. Reset URL: {callbackUrl}", user.UserName, callbackUrl); diff --git a/Server/Areas/Identity/Pages/Account/Login.cshtml.cs b/Server/Areas/Identity/Pages/Account/Login.cshtml.cs index cbe109dd6..d433aa522 100644 --- a/Server/Areas/Identity/Pages/Account/Login.cshtml.cs +++ b/Server/Areas/Identity/Pages/Account/Login.cshtml.cs @@ -38,30 +38,30 @@ public LoginModel(SignInManager signInManager, } [BindProperty] - public InputModel Input { get; set; } + public InputModel Input { get; set; } = null!; - public IList ExternalLogins { get; set; } + public IList? ExternalLogins { get; set; } - public string ReturnUrl { get; set; } + public string? ReturnUrl { get; set; } [TempData] - public string ErrorMessage { get; set; } + public string? ErrorMessage { get; set; } public class InputModel { [Required] [EmailAddress] - public string Email { get; set; } + public required string Email { get; set; } [Required] [DataType(DataType.Password)] - public string Password { get; set; } + public required string Password { get; set; } [Display(Name = "Remember me?")] public bool RememberMe { get; set; } } - public async Task OnGetAsync(string returnUrl = null) + public async Task OnGetAsync(string? returnUrl = null) { if (!string.IsNullOrEmpty(ErrorMessage)) { @@ -78,7 +78,7 @@ public async Task OnGetAsync(string returnUrl = null) ReturnUrl = returnUrl; } - public async Task OnPostAsync(string returnUrl = null) + public async Task OnPostAsync(string? returnUrl = null) { returnUrl ??= Url.Content("~/"); @@ -96,7 +96,7 @@ public async Task OnPostAsync(string returnUrl = null) } if (result.RequiresTwoFactor) { - return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe }); + return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, Input.RememberMe }); } if (result.IsLockedOut) { @@ -108,6 +108,11 @@ public async Task OnPostAsync(string returnUrl = null) if (await _dataService.TempPasswordSignIn(Input.Email, Input.Password)) { var user = await _userManager.FindByNameAsync(Input.Email); + if (user is null) + { + ModelState.AddModelError(string.Empty, "Account not found."); + return Page(); + } var code = await _userManager.GeneratePasswordResetTokenAsync(user); code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); var callbackUrl = Url.Page( @@ -116,7 +121,7 @@ public async Task OnPostAsync(string returnUrl = null) values: new { area = "Identity", code }, protocol: Request.Scheme); - return Redirect(callbackUrl); + return Redirect(callbackUrl!); } ModelState.AddModelError(string.Empty, "Invalid login attempt."); diff --git a/Server/Areas/Identity/Pages/Account/Logout.cshtml b/Server/Areas/Identity/Pages/Account/Logout.cshtml index e84347e52..f320b0194 100644 --- a/Server/Areas/Identity/Pages/Account/Logout.cshtml +++ b/Server/Areas/Identity/Pages/Account/Logout.cshtml @@ -16,7 +16,7 @@ { if (SignInManager.IsSignedIn(User)) { - var activeSessions = RemoteControlSessionCache.Sessions.Where(x => x.RequesterUserName == HttpContext.User.Identity.Name); + var activeSessions = RemoteControlSessionCache.Sessions.Where(x => x.RequesterUserName == HttpContext.User.Identity?.Name); foreach (var session in activeSessions) { await DesktopHubContext.Clients.Client(session.DesktopConnectionId).SendAsync("Disconnect", "User logged out."); diff --git a/Server/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs b/Server/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs index 9148edc86..7a64628e2 100644 --- a/Server/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs +++ b/Server/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs @@ -32,18 +32,19 @@ public EnableAuthenticatorModel( _urlEncoder = urlEncoder; } - public string SharedKey { get; set; } + public string? SharedKey { get; set; } + + public string? AuthenticatorUri { get; set; } - public string AuthenticatorUri { get; set; } [TempData] - public string[] RecoveryCodes { get; set; } + public string[]? RecoveryCodes { get; set; } [TempData] - public string StatusMessage { get; set; } + public string? StatusMessage { get; set; } [BindProperty] - public InputModel Input { get; set; } + public InputModel Input { get; set; } = new(); public class InputModel { @@ -51,7 +52,7 @@ public class InputModel [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] [DataType(DataType.Text)] [Display(Name = "Verification Code")] - public string Code { get; set; } + public string Code { get; set; } = string.Empty; } public async Task OnGetAsync() @@ -103,7 +104,7 @@ public async Task OnPostAsync() if (await _userManager.CountRecoveryCodesAsync(user) == 0) { var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); - RecoveryCodes = recoveryCodes.ToArray(); + RecoveryCodes = recoveryCodes?.ToArray(); return RedirectToPage("./ShowRecoveryCodes"); } else @@ -122,10 +123,10 @@ private async Task LoadSharedKeyAndQrCodeUriAsync(RemotelyUser user) unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); } - SharedKey = FormatKey(unformattedKey); + SharedKey = FormatKey($"{unformattedKey}"); var email = await _userManager.GetEmailAsync(user); - AuthenticatorUri = GenerateQrCodeUri(email, unformattedKey); + AuthenticatorUri = GenerateQrCodeUri($"{email}", $"{unformattedKey}"); } private string FormatKey(string unformattedKey) diff --git a/Server/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs b/Server/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs index b10322bc0..9ee3bd79b 100644 --- a/Server/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs +++ b/Server/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs @@ -23,19 +23,19 @@ public IndexModel( _signInManager = signInManager; } - public string Username { get; set; } + public string? Username { get; set; } [TempData] - public string StatusMessage { get; set; } + public string? StatusMessage { get; set; } [BindProperty] - public InputModel Input { get; set; } + public InputModel Input { get; set; } = new(); public class InputModel { [Phone] [Display(Name = "Phone number")] - public string PhoneNumber { get; set; } + public string? PhoneNumber { get; set; } } private async Task LoadAsync(RemotelyUser user) diff --git a/Server/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs b/Server/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs index aa0c95ff0..4d1010386 100644 --- a/Server/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs +++ b/Server/Areas/Identity/Pages/Account/Manage/ManageNavPages.cs @@ -44,6 +44,6 @@ private static string PageNavClass(ViewContext viewContext, string page) { var activePage = viewContext.ViewData["ActivePage"] as string ?? System.IO.Path.GetFileNameWithoutExtension(viewContext.ActionDescriptor.DisplayName); - return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : null; + return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : ""; } } diff --git a/Server/Auth/LocalOnlyFilter.cs b/Server/Auth/LocalOnlyFilter.cs index ca32340e0..f4a5aae1b 100644 --- a/Server/Auth/LocalOnlyFilter.cs +++ b/Server/Auth/LocalOnlyFilter.cs @@ -9,7 +9,7 @@ public class LocalOnlyFilter : IAuthorizationFilter public void OnAuthorization(AuthorizationFilterContext context) { var remoteIp = context.HttpContext.Connection.RemoteIpAddress; - if (!IPAddress.IsLoopback(remoteIp)) + if (remoteIp is null || !IPAddress.IsLoopback(remoteIp)) { context.Result = new UnauthorizedResult(); return; diff --git a/Server/Auth/TwoFactorRequiredHandler.cs b/Server/Auth/TwoFactorRequiredHandler.cs index 692c352eb..beb431f9d 100644 --- a/Server/Auth/TwoFactorRequiredHandler.cs +++ b/Server/Auth/TwoFactorRequiredHandler.cs @@ -22,10 +22,10 @@ public TwoFactorRequiredHandler(UserManager userManager, IApplicat protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, TwoFactorRequiredRequirement requirement) { - if (context.User.Identity.IsAuthenticated && _appConfig.Require2FA) + if (context.User.Identity?.IsAuthenticated == true && _appConfig.Require2FA) { var user = await _userManager.GetUserAsync(context.User); - if (!user.TwoFactorEnabled) + if (user?.TwoFactorEnabled != true) { context.Fail(); return; diff --git a/Server/Components/AlertBanner.razor b/Server/Components/AlertBanner.razor index f5fb13be0..a1552ee1b 100644 --- a/Server/Components/AlertBanner.razor +++ b/Server/Components/AlertBanner.razor @@ -9,10 +9,10 @@ @code { [Parameter] - public string Message { get; set; } + public string? Message { get; set; } [Parameter] - public string StatusClass { get; set; } + public string? StatusClass { get; set; } [Parameter] public EventCallback OnClose { get; set; } diff --git a/Server/Components/AlertsFrame.razor b/Server/Components/AlertsFrame.razor index adb2c96f3..922ae35a2 100644 --- a/Server/Components/AlertsFrame.razor +++ b/Server/Components/AlertsFrame.razor @@ -57,7 +57,7 @@ private readonly List _alerts = new(); private bool _isOpen; - private string FrameClass => _isOpen ? "open" : null; + private string? FrameClass => _isOpen ? "open" : null; protected override async Task OnInitializedAsync() { @@ -93,7 +93,8 @@ private void ShowAlertDetails(Alert alert) { - ModalService.ShowModal($"Alert Details for {alert.Device?.DeviceName}", alert.Details.Split('\n')); + var body = alert.Details?.Split('\n') ?? Array.Empty(); + ModalService.ShowModal($"Alert Details for {alert.Device?.DeviceName}", body); } private void ToggleOpen() diff --git a/Server/Components/ColorPicker.razor b/Server/Components/ColorPicker.razor index 40c3615ee..d6fd662db 100644 --- a/Server/Components/ColorPicker.razor +++ b/Server/Components/ColorPicker.razor @@ -20,7 +20,7 @@ @code { - private ColorPickerModel _color; + private ColorPickerModel _color = new(); [Parameter] public ColorPickerModel Color diff --git a/Server/Components/Devices/ChatCard.razor b/Server/Components/Devices/ChatCard.razor index c89da48d3..4cf7ef115 100644 --- a/Server/Components/Devices/ChatCard.razor +++ b/Server/Components/Devices/ChatCard.razor @@ -6,9 +6,9 @@
- @Session?.MissedChats + @Session.MissedChats - @Session?.DeviceName + @Session.DeviceName
@code { - private System.Timers.Timer _collapseTimer; - private string _showClass; + private System.Timers.Timer? _collapseTimer; + private string? _showClass; private bool _isExpanded; [Parameter] - public string ButtonClass { get; set; } + public string? ButtonClass { get; set; } [Parameter] - public RenderFragment ButtonContent { get; set; } + public RenderFragment? ButtonContent { get; set; } [Parameter] - public string DropDownClass { get; set; } + public string? DropDownClass { get; set; } [Parameter] - public string DropDownMenuClass { get; set; } + public string? DropDownMenuClass { get; set; } [Parameter] - public RenderFragment ChildListItems { get; set; } + public RenderFragment? ChildListItems { get; set; } private void ToggleShown() { diff --git a/Server/Components/FileInputButton.razor b/Server/Components/FileInputButton.razor index 4e1bae25a..cbd2dd74a 100644 --- a/Server/Components/FileInputButton.razor +++ b/Server/Components/FileInputButton.razor @@ -8,14 +8,14 @@ @code { [Parameter] - public string ClassNames { get; set; } + public string? ClassNames { get; set; } [Parameter] - public RenderFragment ButtonContent { get; set; } + public RenderFragment? ButtonContent { get; set; } [Parameter] - public Func OnChanged { get; set; } + public Func? OnChanged { get; set; } [Parameter] public bool Multiple { get; set; } diff --git a/Server/Components/ModalContents/EditDeviceGroup.razor b/Server/Components/ModalContents/EditDeviceGroup.razor index 95603a8a7..3d81f6755 100644 --- a/Server/Components/ModalContents/EditDeviceGroup.razor +++ b/Server/Components/ModalContents/EditDeviceGroup.razor @@ -21,10 +21,10 @@ public static string DeviceGroupsPropName => nameof(DeviceGroups); [Parameter] - public RemotelyUser EditUser { get; set; } + public required RemotelyUser EditUser { get; set; } [Parameter] - public DeviceGroup[] DeviceGroups { get; set; } + public required DeviceGroup[] DeviceGroups { get; set; } private bool DoesGroupContainUser(DeviceGroup group) @@ -34,7 +34,9 @@ private async Task GroupCheckChanged(ChangeEventArgs args, DeviceGroup group) { - if ((bool)args.Value) + if (!string.IsNullOrWhiteSpace(EditUser.UserName) && + args.Value is bool boolValue && + boolValue) { if (!DataService.AddUserToDeviceGroup(EditUser.OrganizationID, group.ID, EditUser.UserName, out var result)) { diff --git a/Server/Components/ModalHarness.razor b/Server/Components/ModalHarness.razor index 910aa2f6a..6d2843639 100644 --- a/Server/Components/ModalHarness.razor +++ b/Server/Components/ModalHarness.razor @@ -11,13 +11,13 @@
@code { - private string _showClass; - private string _displayStyle; + private string? _showClass; + private string? _displayStyle; protected override Task OnAfterRenderAsync(bool firstRender) { diff --git a/Server/Components/Scripts/SavedScripts.razor b/Server/Components/Scripts/SavedScripts.razor index ea70493e8..d688e8817 100644 --- a/Server/Components/Scripts/SavedScripts.razor +++ b/Server/Components/Scripts/SavedScripts.razor @@ -13,7 +13,7 @@
- +
diff --git a/Server/Components/TabControl/TabContent.razor b/Server/Components/TabControl/TabContent.razor index aa9a60b0f..9713a215a 100644 --- a/Server/Components/TabControl/TabContent.razor +++ b/Server/Components/TabControl/TabContent.razor @@ -8,13 +8,14 @@ @code { [CascadingParameter] - public TabControl Parent { get; set; } + public required TabControl Parent { get; init; } [Parameter] - public RenderFragment ChildContent { get; set; } + public RenderFragment? ChildContent { get; set; } [Parameter] - public string Name { get; set; } + [EditorRequired] + public required string Name { get; set; } private bool IsActive => Parent.ActiveTab == Name; diff --git a/Server/Components/TabControl/TabControl.razor b/Server/Components/TabControl/TabControl.razor index 4b140ab4b..139f7d6e7 100644 --- a/Server/Components/TabControl/TabControl.razor +++ b/Server/Components/TabControl/TabControl.razor @@ -12,15 +12,15 @@ @code { [Parameter] - public RenderFragment TabHeaders { get; set; } + public RenderFragment? TabHeaders { get; set; } [Parameter] - public RenderFragment TabContents { get; set; } + public RenderFragment? TabContents { get; set; } [Parameter] - public string InitialActiveTab { get; set; } + public string? InitialActiveTab { get; set; } - public string ActiveTab { get; set; } + public string? ActiveTab { get; set; } protected override void OnInitialized() { diff --git a/Server/Components/TabControl/TabHeader.razor b/Server/Components/TabControl/TabHeader.razor index ebbf6fad4..80238cd3c 100644 --- a/Server/Components/TabControl/TabHeader.razor +++ b/Server/Components/TabControl/TabHeader.razor @@ -8,21 +8,21 @@ @code { [CascadingParameter] - public TabControl Parent { get; set; } + public TabControl? Parent { get; set; } [Parameter] - public RenderFragment ChildContent { get; set; } + public RenderFragment? ChildContent { get; set; } [Parameter] - public Action OnActivated { get; set; } + public Action? OnActivated { get; set; } [Parameter] - public string NavigationUri { get; set; } + public string? NavigationUri { get; set; } [Parameter] - public string Name { get; set; } + public string? Name { get; set; } - private string ActiveClass => Parent.ActiveTab == Name ? "active" : ""; + private string ActiveClass => Parent?.ActiveTab == Name ? "active" : ""; protected override async Task OnInitializedAsync() { @@ -46,7 +46,7 @@ } else { - Parent.SetActiveTab(this); + Parent?.SetActiveTab(this); StateHasChanged(); OnActivated?.Invoke(); } diff --git a/Server/Components/ToastHarness.razor b/Server/Components/ToastHarness.razor index 79dc35b4e..85484e060 100644 --- a/Server/Components/ToastHarness.razor +++ b/Server/Components/ToastHarness.razor @@ -2,7 +2,7 @@ @inject IToastService ToastService
- @foreach (var toast in ToastService?.Toasts) + @foreach (var toast in ToastService.Toasts) {
@toast.Message diff --git a/Server/Components/TreeView/TreeView.razor.cs b/Server/Components/TreeView/TreeView.razor.cs index 261e72fb9..694cf9046 100644 --- a/Server/Components/TreeView/TreeView.razor.cs +++ b/Server/Components/TreeView/TreeView.razor.cs @@ -9,35 +9,41 @@ namespace Remotely.Server.Components.TreeView; public partial class TreeView : ComponentBase { [Parameter] - public IEnumerable DataSource { get; set; } + [EditorRequired] + public required IEnumerable DataSource { get; set; } [Parameter] - public Func> ChildItemSelector { get; set; } + [EditorRequired] + public required Func> ChildItemSelector { get; set; } [Parameter] - public Func ItemHeaderSelector { get; set; } + [EditorRequired] + public required Func ItemHeaderSelector { get; set; } [Parameter] - public Func KeySelector { get; set; } + [EditorRequired] + public required Func KeySelector { get; set; } [Parameter] public EventCallback ItemSelected { get; set; } [Parameter] - public string WrapperStyle { get; set; } + public string? WrapperStyle { get; set; } [Parameter] - public string ChildItemStyle { get; set; } + public string? ChildItemStyle { get; set; } [Parameter] public int IndentLevel { get; set; } [Parameter] - public Func ItemTypeSelector { get; set; } + [EditorRequired] + public required Func ItemTypeSelector { get; set; } [Parameter] - public Func ItemIconCssSelector { get; set; } + [EditorRequired] + public required Func? ItemIconCssSelector { get; set; } - public TreeViewItem SelectedNode { get; set; } + public TreeViewItem? SelectedNode { get; set; } } diff --git a/Server/Components/TreeView/TreeViewItem.razor.cs b/Server/Components/TreeView/TreeViewItem.razor.cs index 7c6e6f138..7eb1462ef 100644 --- a/Server/Components/TreeView/TreeViewItem.razor.cs +++ b/Server/Components/TreeView/TreeViewItem.razor.cs @@ -9,34 +9,40 @@ namespace Remotely.Server.Components.TreeView; public partial class TreeViewItem : ComponentBase { [CascadingParameter] - public TreeView ParentTree { get; set; } + public required TreeView ParentTree { get; set; } [Parameter] - public T Source { get; set; } + [EditorRequired] + public required T Source { get; set; } [Parameter] - public Func> ChildItemSelector { get; set; } + [EditorRequired] + public required Func> ChildItemSelector { get; set; } [Parameter] - public Func HeaderSelector { get; set; } + [EditorRequired] + public required Func HeaderSelector { get; set; } [Parameter] - public Func ItemIconCssSelector { get; set; } + [EditorRequired] + public required Func ItemIconCssSelector { get; set; } [Parameter] - public string Style { get; set; } + public string? Style { get; set; } [Parameter] public int IndentLevel { get; set; } [Parameter] - public Func ItemTypeSelector { get; set; } + [EditorRequired] + public required Func ItemTypeSelector { get; set; } [Parameter] public EventCallback ItemSelected { get; set; } [Parameter] - public Func KeySelector { get; set; } + [EditorRequired] + public required Func KeySelector { get; set; } public bool IsExpanded { get; set; } diff --git a/Server/Hubs/AgentHub.cs b/Server/Hubs/AgentHub.cs index 811620c33..e24ec7df2 100644 --- a/Server/Hubs/AgentHub.cs +++ b/Server/Hubs/AgentHub.cs @@ -10,9 +10,7 @@ using Remotely.Shared.Models; using Remotely.Shared.Utilities; using System; -using System.Collections.Concurrent; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; @@ -52,7 +50,14 @@ private Device Device { get { - return Context.Items["Device"] as Device; + if (Context.Items["Device"] is Device device) + { + return device; + } + else + { + throw new InvalidOperationException("Device not set."); + } } set { @@ -64,7 +69,7 @@ public Task Chat(string message, bool disconnected, string browserConnectionId) { if (_circuitManager.TryGetConnection(browserConnectionId, out var connection)) { - return connection.InvokeCircuitEvent(CircuitEventName.ChatReceived, Device.ID, Device.DeviceName, message, disconnected); + return connection.InvokeCircuitEvent(CircuitEventName.ChatReceived, Device.ID, $"{Device.DeviceName}", message, disconnected); } else { @@ -102,7 +107,7 @@ public async Task DeviceCameOnline(DeviceClientDto device) { ip = ip.MapToIPv4(); } - device.PublicIP = ip?.ToString(); + device.PublicIP = $"{ip}"; if (CheckForDeviceBan(device.PublicIP)) { @@ -157,7 +162,7 @@ public async Task DeviceHeartbeat(DeviceClientDto device) { ip = ip.MapToIPv4(); } - device.PublicIP = ip?.ToString(); + device.PublicIP = $"{ip}"; if (CheckForDeviceBan(device.PublicIP)) { @@ -216,10 +221,10 @@ public string GetServerUrl() public string GetServerVerificationToken() { - return Device.ServerVerificationToken; + return $"{Device.ServerVerificationToken}"; } - public override Task OnDisconnectedAsync(Exception exception) + public override Task OnDisconnectedAsync(Exception? exception) { try { diff --git a/Server/Hubs/CircuitConnection.cs b/Server/Hubs/CircuitConnection.cs index bde56c2bd..e50ee796a 100644 --- a/Server/Hubs/CircuitConnection.cs +++ b/Server/Hubs/CircuitConnection.cs @@ -28,7 +28,7 @@ namespace Remotely.Server.Hubs; public interface ICircuitConnection { event EventHandler? MessageReceived; - RemotelyUser? User { get; } + RemotelyUser User { get; } Task DeleteRemoteLogs(string deviceId); diff --git a/Server/Models/ApiLogin.cs b/Server/Models/ApiLogin.cs index da4c37b30..45dfc014c 100644 --- a/Server/Models/ApiLogin.cs +++ b/Server/Models/ApiLogin.cs @@ -2,6 +2,6 @@ public class ApiLogin { - public string Email { get; set; } - public string Password { get; set; } + public string? Email { get; set; } + public string? Password { get; set; } } diff --git a/Server/Models/ModalButton.cs b/Server/Models/ModalButton.cs index 81f28fc0e..88ff556c3 100644 --- a/Server/Models/ModalButton.cs +++ b/Server/Models/ModalButton.cs @@ -4,8 +4,8 @@ namespace Remotely.Server.Models; public class ModalButton { - public string Class { get; set; } - public string Text { get; set; } + public string Class { get; init; } = string.Empty; + public string Text { get; init; } = string.Empty; - public Action OnClick { get; set; } + public required Action OnClick { get; init; } } diff --git a/Server/Models/RemoteControlRequest.cs b/Server/Models/RemoteControlRequest.cs index 43d0c14fd..0b104f1ad 100644 --- a/Server/Models/RemoteControlRequest.cs +++ b/Server/Models/RemoteControlRequest.cs @@ -2,7 +2,7 @@ public class RemoteControlRequest { - public string DeviceID { get; set; } - public string Email { get; set; } - public string Password { get; set; } + public string? DeviceID { get; set; } + public string? Email { get; set; } + public string? Password { get; set; } } diff --git a/Server/Pages/Branding.razor b/Server/Pages/Branding.razor index 2b9a26f72..f0ae890db 100644 --- a/Server/Pages/Branding.razor +++ b/Server/Pages/Branding.razor @@ -65,8 +65,8 @@
@code { - private string _alertMessage; - private string _base64Icon; + private string? _alertMessage; + private string? _base64Icon; private InputModel _inputModel = new(); private class InputModel @@ -74,13 +74,13 @@ [StringLength(25)] [Required] [Display(Name = "Product Name")] - public string ProductName { get; set; } + public string ProductName { get; set; } = string.Empty; public ColorPickerModel TitleForegroundColor { get; set; } = new(); public ColorPickerModel TitleBackgroundColor { get; set; } = new(); public ColorPickerModel TitleButtonColor { get; set; } = new(); - public byte[] IconBytes { get; set; } + public byte[] IconBytes { get; set; } = Array.Empty(); } protected override async Task OnInitializedAsync() diff --git a/Server/Pages/DeviceDetails.razor b/Server/Pages/DeviceDetails.razor index f8979bc2c..c6a97118f 100644 --- a/Server/Pages/DeviceDetails.razor +++ b/Server/Pages/DeviceDetails.razor @@ -255,9 +255,9 @@ else @scriptResult.TimeStamp @scriptResult.SenderUserName @scriptResult.RunTime - @GetTrimmedText(scriptResult.ScriptInput, 25) - @GetTrimmedText(scriptResult.StandardOutput, 25) - @GetTrimmedText(scriptResult.ErrorOutput, 25) + @GetTrimmedText($"{scriptResult.ScriptInput}", 25) + @GetTrimmedText($"{scriptResult.StandardOutput}", 25) + @GetTrimmedText($"{scriptResult.ErrorOutput}", 25) diff --git a/Server/Pages/Error.cshtml.cs b/Server/Pages/Error.cshtml.cs index 236f89a17..556e40456 100644 --- a/Server/Pages/Error.cshtml.cs +++ b/Server/Pages/Error.cshtml.cs @@ -13,17 +13,10 @@ namespace Remotely.Server.Pages; [IgnoreAntiforgeryToken] public class ErrorModel : PageModel { - public string RequestId { get; set; } + public string? RequestId { get; set; } public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); - private readonly ILogger _logger; - - public ErrorModel(ILogger logger) - { - _logger = logger; - } - public void OnGet() { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; diff --git a/Server/Pages/GetSupport.cshtml.cs b/Server/Pages/GetSupport.cshtml.cs index 216eb0415..b9216631b 100644 --- a/Server/Pages/GetSupport.cshtml.cs +++ b/Server/Pages/GetSupport.cshtml.cs @@ -21,7 +21,7 @@ public GetSupportModel(IDataService dataService, IEmailSenderEx emailSender) public string? StatusMessage { get; set; } [BindProperty] - public InputModel Input { get; set; } + public InputModel Input { get; set; } = new(); public IActionResult OnGet() { @@ -76,7 +76,7 @@ public class InputModel { [StringLength(150)] [Required] - public required string Name { get; set; } + public string Name { get; set; } = string.Empty; public string? Email { get; set; } public string? Phone { get; set; } public bool ChatResponseOk { get; set; } diff --git a/Server/Pages/ScriptsPage.razor b/Server/Pages/ScriptsPage.razor index b9459bc72..6e7cfaf05 100644 --- a/Server/Pages/ScriptsPage.razor +++ b/Server/Pages/ScriptsPage.razor @@ -39,7 +39,7 @@ private bool _showOnlyMyScripts = true; [Parameter] - public string ActiveTab { get; set; } + public string? ActiveTab { get; set; } public bool ShowOnlyMyScripts { @@ -56,7 +56,7 @@ { get { - if (_treeNodes?.Any() == true) + if (_treeNodes.Any() == true) { return _treeNodes; } diff --git a/Server/Pages/ServerConfig.razor.cs b/Server/Pages/ServerConfig.razor.cs index e8f3584c4..c6d7f6945 100644 --- a/Server/Pages/ServerConfig.razor.cs +++ b/Server/Pages/ServerConfig.razor.cs @@ -58,7 +58,7 @@ public class AppSettingsModel [Display(Name = "Max Organizations")] public int MaxOrganizationCount { get; set; } [Display(Name = "Message of the Day")] - public string MessageOfTheDay { get; set; } + public string? MessageOfTheDay { get; set; } [Display(Name = "Redirect To HTTPS")] public bool RedirectToHttps { get; set; } @@ -76,28 +76,28 @@ public class AppSettingsModel public bool Require2FA { get; set; } [Display(Name = "SMTP Display Name")] - public string SmtpDisplayName { get; set; } + public string? SmtpDisplayName { get; set; } [Display(Name = "SMTP Email")] [EmailAddress] - public string SmtpEmail { get; set; } + public string? SmtpEmail { get; set; } [Display(Name = "SMTP Host")] - public string SmtpHost { get; set; } + public string? SmtpHost { get; set; } [Display(Name = "SMTP Local Domain")] - public string SmtpLocalDomain { get; set; } + public string? SmtpLocalDomain { get; set; } [Display(Name = "SMTP Check Certificate Revocation")] public bool SmtpCheckCertificateRevocation { get; set; } [Display(Name = "SMTP Password")] - public string SmtpPassword { get; set; } + public string? SmtpPassword { get; set; } [Display(Name = "SMTP Port")] public int SmtpPort { get; set; } [Display(Name = "SMTP Username")] - public string SmtpUserName { get; set; } + public string? SmtpUserName { get; set; } [Display(Name = "Theme")] [JsonConverter(typeof(JsonStringEnumConverter))] @@ -116,72 +116,74 @@ public class AppSettingsModel public class ConnectionStringsModel { [Display(Name = "PostgreSQL")] - public string PostgreSQL { get; set; } + public string? PostgreSQL { get; set; } [Display(Name = "SQLite")] - public string SQLite { get; set; } + public string? SQLite { get; set; } [Display(Name = "SQL Server")] - public string SQLServer { get; set; } + public string? SQLServer { get; set; } } public partial class ServerConfig : AuthComponentBase { - private string _alertMessage; - private string _bannedDeviceSelected; - private string _bannedDeviceToAdd; + private string? _alertMessage; + private string? _bannedDeviceSelected; + private string? _bannedDeviceToAdd; - private string _knownProxySelected; - private string _knownProxyToAdd; + private string? _knownProxySelected; + private string? _knownProxyToAdd; private bool _showMyOrgAdminsOnly = true; private bool _showAdminsOnly; - private string _trustedCorsOriginSelected; - private string _trustedCorsOriginToAdd; + private string? _trustedCorsOriginSelected; + private string? _trustedCorsOriginToAdd; private readonly List _userList = new(); [Inject] - private IHubContext AgentHubContext { get; set; } + private IHubContext AgentHubContext { get; init; } = null!; [Inject] - private IConfiguration Configuration { get; set; } + private IConfiguration Configuration { get; init; } = null!; private ConnectionStringsModel ConnectionStrings { get; } = new(); [Inject] - private IDataService DataService { get; set; } + private IDataService DataService { get; init; } = null!; [Inject] - private IEmailSenderEx EmailSender { get; set; } + private IEmailSenderEx EmailSender { get; init; } = null!; [Inject] - private IWebHostEnvironment HostEnv { get; set; } + private IWebHostEnvironment HostEnv { get; init; } = null!; [Inject] - private ILogger Logger { get; set; } + private ILogger Logger { get; init; } = null!; [Inject] - private IAgentHubSessionCache ServiceSessionCache { get; init; } + private IAgentHubSessionCache ServiceSessionCache { get; init; } = null!; private AppSettingsModel Input { get; } = new(); [Inject] - private IModalService ModalService { get; set; } + private IModalService ModalService { get; init; } = null!; [Inject] - private IUpgradeService UpgradeService { get; init; } + private IUpgradeService UpgradeService { get; init; } = null!; [Inject] - private ICircuitManager CircuitManager { get; set; } + private ICircuitManager CircuitManager { get; init; } = null!; private IEnumerable OutdatedDevices => GetOutdatedDevices(); [Inject] - private IToastService ToastService { get; set; } + private IToastService ToastService { get; init; } = null!; + private int TotalDevices => DataService.GetTotalDevices(); + private IEnumerable UserList { get @@ -338,6 +340,12 @@ private async Task SaveAndTestSmtpSettings() { await SaveInputToAppSettings(); + if (string.IsNullOrWhiteSpace(User.Email)) + { + ToastService.ShowToast2("User email is not set.", Enums.ToastType.Warning); + return; + } + var success = await EmailSender.SendEmailAsync(User.Email, "Remotely Test Email", "Congratulations! Your SMTP settings are working!", User.OrganizationID); if (success) { @@ -359,19 +367,27 @@ private async Task SaveInputToAppSettings() var devSettings = HostEnv.ContentRootFileProvider.GetFileInfo("appsettings.Development.json"); var settings = HostEnv.ContentRootFileProvider.GetFileInfo("appsettings.json"); - if (HostEnv.IsProduction() && prodSettings.Exists) + if (HostEnv.IsProduction() + && prodSettings.Exists && + !string.IsNullOrWhiteSpace(prodSettings.PhysicalPath)) { savePath = prodSettings.PhysicalPath; } - else if (HostEnv.IsStaging() && stagingSettings.Exists) + else if ( + HostEnv.IsStaging() && + stagingSettings.Exists && + !string.IsNullOrWhiteSpace(stagingSettings.PhysicalPath)) { savePath = stagingSettings.PhysicalPath; } - else if (HostEnv.IsDevelopment() && devSettings.Exists) + else if ( + HostEnv.IsDevelopment() && + devSettings.Exists && + !string.IsNullOrWhiteSpace(devSettings.PhysicalPath)) { savePath = devSettings.PhysicalPath; } - else if (settings.Exists) + else if (settings.Exists && !string.IsNullOrWhiteSpace(settings.PhysicalPath)) { savePath = settings.PhysicalPath; } @@ -381,6 +397,10 @@ private async Task SaveInputToAppSettings() } var settingsJson = JsonSerializer.Deserialize>(await System.IO.File.ReadAllTextAsync(savePath)); + if (settingsJson is null) + { + return; + } settingsJson["ApplicationOptions"] = Input; settingsJson["ConnectionStrings"] = ConnectionStrings; @@ -393,7 +413,10 @@ private async Task SaveInputToAppSettings() } private void SetIsServerAdmin(ChangeEventArgs ev, RemotelyUser user) { - var isAdmin = (bool)ev.Value; + if (ev.Value is not bool isAdmin) + { + return; + } DataService.SetIsServerAdmin(user.Id, isAdmin, User.Id); ToastService.ShowToast("Server admins updated."); } @@ -404,10 +427,13 @@ private void ShowOutdatedDevices() { var outdatedDeviceNames = DataService .GetDevices(OutdatedDevices) - .Select(x => x.DeviceName); + .Select(x => $"{x.DeviceName}"); + + var body = (new[] { "Outdated Devices:" }) + .Concat(outdatedDeviceNames) + .ToArray(); - ModalService.ShowModal("Outdated Devices", - (new[] { "Outdated Devices:" }).Concat(outdatedDeviceNames).ToArray()); + ModalService.ShowModal("Outdated Devices", body); } else { diff --git a/Server/Pages/UserOptions.razor b/Server/Pages/UserOptions.razor index 19e1b8930..3f5fa6ae4 100644 --- a/Server/Pages/UserOptions.razor +++ b/Server/Pages/UserOptions.razor @@ -92,7 +92,7 @@ @code { private RemotelyUserOptions _options = new(); - private string _alertMessage; + private string? _alertMessage; protected override async Task OnInitializedAsync() { @@ -119,7 +119,7 @@ _options.CommandModeShortcutWinPS = "/" + _options.CommandModeShortcutWinPS; } - DataService.UpdateUserOptions(User.UserName, _options); + DataService.UpdateUserOptions(UserName, _options); _alertMessage = "Options saved"; diff --git a/Server/Services/AgentHubSessionCache.cs b/Server/Services/AgentHubSessionCache.cs index 1f7b98abe..4ad82cbdf 100644 --- a/Server/Services/AgentHubSessionCache.cs +++ b/Server/Services/AgentHubSessionCache.cs @@ -2,6 +2,7 @@ using Remotely.Shared.Models; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; namespace Remotely.Server.Services; @@ -13,9 +14,9 @@ public interface IAgentHubSessionCache ICollection GetAllDevices(); IEnumerable GetConnectionIdsByDeviceIds(IEnumerable deviceIds); - bool TryGetByDeviceId(string deviceId, out Device device); - bool TryGetConnectionId(string deviceId, out string serviceConnectionId); - bool TryRemoveByConnectionId(string connectionId, out Device device); + bool TryGetByDeviceId(string deviceId, [NotNullWhen(true)] out Device? device); + bool TryGetConnectionId(string deviceId, [NotNullWhen(true)] out string? serviceConnectionId); + bool TryRemoveByConnectionId(string connectionId, [NotNullWhen(true)] out Device? device); } public class AgentHubSessionCache : IAgentHubSessionCache @@ -55,23 +56,23 @@ public IEnumerable GetConnectionIdsByDeviceIds(IEnumerable devic } } - public bool TryGetByDeviceId(string deviceId, out Device device) + public bool TryGetByDeviceId(string deviceId, [NotNullWhen(true)] out Device? device) { if (_deviceIdToConnectionIdLookup.TryGetValue(deviceId, out var connectionId) && _connectionIdToDeviceLookup.TryGetValue(connectionId, out device)) { return true; } - device = Device.Empty; + device = default; return false; } - public bool TryGetConnectionId(string deviceId, out string serviceConnectionId) + public bool TryGetConnectionId(string deviceId, [NotNullWhen(true)] out string? serviceConnectionId) { return _deviceIdToConnectionIdLookup.TryGetValue(deviceId, out serviceConnectionId); } - public bool TryRemoveByConnectionId(string connectionId, out Device device) + public bool TryRemoveByConnectionId(string connectionId, [NotNullWhen(true)] out Device? device) { if (_connectionIdToDeviceLookup.TryRemove(connectionId, out var lookupResult)) { @@ -80,7 +81,7 @@ public bool TryRemoveByConnectionId(string connectionId, out Device device) return true; } - device = Device.Empty; + device = null; return false; } } diff --git a/Server/Services/ApplicationConfig.cs b/Server/Services/ApplicationConfig.cs index 24deb626a..9fd583882 100644 --- a/Server/Services/ApplicationConfig.cs +++ b/Server/Services/ApplicationConfig.cs @@ -59,21 +59,21 @@ public ApplicationConfig(IConfiguration config) public string[] KnownProxies => _config.GetSection("ApplicationOptions:KnownProxies").Get() ?? System.Array.Empty(); public int MaxConcurrentUpdates => int.TryParse(_config["ApplicationOptions:MaxConcurrentUpdates"], out var result) ? result : 10; public int MaxOrganizationCount => int.TryParse(_config["ApplicationOptions:MaxOrganizationCount"], out var result) ? result : 1; - public string MessageOfTheDay => _config["ApplicationOptions:MessageOfTheDay"]; + public string MessageOfTheDay => _config["ApplicationOptions:MessageOfTheDay"] ?? string.Empty; public bool RedirectToHttps => bool.TryParse(_config["ApplicationOptions:RedirectToHttps"], out var result) && result; public bool RemoteControlNotifyUser => bool.TryParse(_config["ApplicationOptions:RemoteControlNotifyUser"], out var result) && result; public bool RemoteControlRequiresAuthentication => bool.TryParse(_config["ApplicationOptions:RemoteControlRequiresAuthentication"], out var result) && result; public int RemoteControlSessionLimit => int.TryParse(_config["ApplicationOptions:RemoteControlSessionLimit"], out var result) ? result : 3; public bool Require2FA => bool.TryParse(_config["ApplicationOptions:Require2FA"], out var result) && result; - public string ServerUrl => _config["ApplicationOptions:ServerUrl"]; + public string ServerUrl => _config["ApplicationOptions:ServerUrl"] ?? string.Empty; public bool SmtpCheckCertificateRevocation => !bool.TryParse(_config["ApplicationOptions:SmtpCheckCertificateRevocation"], out var result) || result; - public string SmtpDisplayName => _config["ApplicationOptions:SmtpDisplayName"]; - public string SmtpEmail => _config["ApplicationOptions:SmtpEmail"]; - public string SmtpHost => _config["ApplicationOptions:SmtpHost"]; - public string SmtpLocalDomain => _config["ApplicationOptions:SmtpLocalDomain"]; - public string SmtpPassword => _config["ApplicationOptions:SmtpPassword"]; + public string SmtpDisplayName => _config["ApplicationOptions:SmtpDisplayName"] ?? string.Empty; + public string SmtpEmail => _config["ApplicationOptions:SmtpEmail"] ?? string.Empty; + public string SmtpHost => _config["ApplicationOptions:SmtpHost"] ?? string.Empty; + public string SmtpLocalDomain => _config["ApplicationOptions:SmtpLocalDomain"] ?? string.Empty; + public string SmtpPassword => _config["ApplicationOptions:SmtpPassword"] ?? string.Empty; public int SmtpPort => int.TryParse(_config["ApplicationOptions:SmtpPort"], out var result) ? result : 25; - public string SmtpUserName => _config["ApplicationOptions:SmtpUserName"]; + public string SmtpUserName => _config["ApplicationOptions:SmtpUserName"] ?? string.Empty; public Theme Theme => Enum.TryParse(_config["ApplicationOptions:Theme"], out var result) ? result : Theme.Dark; public string[] TrustedCorsOrigins => _config.GetSection("ApplicationOptions:TrustedCorsOrigins").Get() ?? System.Array.Empty(); public bool UseHsts => bool.TryParse(_config["ApplicationOptions:UseHsts"], out var result) && result; diff --git a/Server/Services/ClientAppState.cs b/Server/Services/ClientAppState.cs index 95d283d3b..0994f2ecd 100644 --- a/Server/Services/ClientAppState.cs +++ b/Server/Services/ClientAppState.cs @@ -14,7 +14,7 @@ public interface IClientAppState : INotifyPropertyChanged, IInvokePropertyChange { ConcurrentList DevicesFrameChatSessions { get; } DeviceCardState DevicesFrameFocusedCardState { get; set; } - string DevicesFrameFocusedDevice { get; set; } + string? DevicesFrameFocusedDevice { get; set; } ConcurrentList DevicesFrameSelectedDevices { get; } ConcurrentQueue TerminalLines { get; } @@ -49,7 +49,7 @@ public DeviceCardState DevicesFrameFocusedCardState set => Set(value); } - public string DevicesFrameFocusedDevice + public string? DevicesFrameFocusedDevice { get => Get(); set => Set(value); diff --git a/Server/Services/DataService.cs b/Server/Services/DataService.cs index bf6dc3842..8e3ef98b3 100644 --- a/Server/Services/DataService.cs +++ b/Server/Services/DataService.cs @@ -352,7 +352,7 @@ public async Task> AddInvite(string orgId, InviteViewModel in var inviteLink = new InviteLink() { - InvitedUser = invite.InvitedUser.ToLower(), + InvitedUser = invite.InvitedUser?.ToLower(), DateSent = DateTimeOffset.Now, IsAdmin = invite.IsAdmin, Organization = organization, diff --git a/Server/Services/EmailSender.cs b/Server/Services/EmailSender.cs index ed2eeb693..656cc85d0 100644 --- a/Server/Services/EmailSender.cs +++ b/Server/Services/EmailSender.cs @@ -13,8 +13,8 @@ namespace Remotely.Server.Services; public interface IEmailSenderEx { - Task SendEmailAsync(string email, string replyTo, string subject, string htmlMessage, string organizationID = null); - Task SendEmailAsync(string email, string subject, string htmlMessage, string organizationID = null); + Task SendEmailAsync(string email, string replyTo, string subject, string htmlMessage, string? organizationID = null); + Task SendEmailAsync(string email, string subject, string htmlMessage, string? organizationID = null); } public class EmailSender : IEmailSender @@ -44,7 +44,12 @@ public EmailSenderEx( _appConfig = appConfig; _logger = logger; } - public async Task SendEmailAsync(string toEmail, string replyTo, string subject, string htmlMessage, string organizationID = null) + public async Task SendEmailAsync( + string toEmail, + string replyTo, + string subject, + string htmlMessage, + string? organizationID = null) { try { @@ -89,7 +94,7 @@ public async Task SendEmailAsync(string toEmail, string replyTo, string su } } - public Task SendEmailAsync(string email, string subject, string htmlMessage, string organizationID = null) + public Task SendEmailAsync(string email, string subject, string htmlMessage, string? organizationID = null) { return SendEmailAsync(email, _appConfig.SmtpEmail, subject, htmlMessage, organizationID); } diff --git a/Server/Services/ModalService.cs b/Server/Services/ModalService.cs index 29c22323b..b6d297c9a 100644 --- a/Server/Services/ModalService.cs +++ b/Server/Services/ModalService.cs @@ -11,24 +11,25 @@ public interface IModalService { event EventHandler ModalShown; List Buttons { get; } - string[] Body { get; } - RenderFragment RenderBody { get; } + string[]? Body { get; } + RenderFragment? RenderBody { get; } string Title { get; } - Task ShowModal(string title, string[] body, ModalButton[] buttons = null); - Task ShowModal(string title, RenderFragment body, ModalButton[] buttons = null); + Task ShowModal(string title, string[] body, ModalButton[]? buttons = null); + Task ShowModal(string title, RenderFragment body, ModalButton[]? buttons = null); } public class ModalService : IModalService { private readonly SemaphoreSlim _modalLock = new(1, 1); - public event EventHandler ModalShown; + public event EventHandler? ModalShown; + public List Buttons { get; } = new List(); - public string[] Body { get; private set; } - public RenderFragment RenderBody { get; private set; } + public string[]? Body { get; private set; } + public RenderFragment? RenderBody { get; private set; } public bool ShowInput { get; private set; } - public string Title { get; private set; } - public async Task ShowModal(string title, string[] body, ModalButton[] buttons = null) + public string Title { get; private set; } = string.Empty; + public async Task ShowModal(string title, string[] body, ModalButton[]? buttons = null) { try { @@ -41,7 +42,7 @@ public async Task ShowModal(string title, string[] body, ModalButton[] buttons = { Buttons.AddRange(buttons); } - ModalShown?.Invoke(this, null); + ModalShown?.Invoke(this, EventArgs.Empty); } finally { @@ -49,7 +50,7 @@ public async Task ShowModal(string title, string[] body, ModalButton[] buttons = } } - public async Task ShowModal(string title, RenderFragment body, ModalButton[] buttons = null) + public async Task ShowModal(string title, RenderFragment body, ModalButton[]? buttons = null) { try { @@ -62,7 +63,7 @@ public async Task ShowModal(string title, RenderFragment body, ModalButton[] but { Buttons.AddRange(buttons); } - ModalShown?.Invoke(this, null); + ModalShown?.Invoke(this, EventArgs.Empty); } finally { diff --git a/Server/Services/OtpProvider.cs b/Server/Services/OtpProvider.cs index ad11051d0..525f3e282 100644 --- a/Server/Services/OtpProvider.cs +++ b/Server/Services/OtpProvider.cs @@ -30,7 +30,8 @@ public string GetOtp(string deviceId) public bool OtpMatchesDevice(string otp, string deviceId) { - if (_otpCache.TryGetValue(otp, out string cachedDevice) && + if (_otpCache.TryGetValue(otp, out var cachedItem) && + cachedItem is string cachedDevice && cachedDevice == deviceId) { return true; diff --git a/Server/Services/ScriptScheduler.cs b/Server/Services/ScriptScheduler.cs index 680c9edf4..9b5d0fc94 100644 --- a/Server/Services/ScriptScheduler.cs +++ b/Server/Services/ScriptScheduler.cs @@ -21,8 +21,8 @@ public class ScriptScheduler : IHostedService, IDisposable TimeSpan.FromSeconds(30) : TimeSpan.FromMinutes(10); - private IServiceProvider _serviceProvider; - private System.Timers.Timer _schedulerTimer; + private readonly IServiceProvider _serviceProvider; + private System.Timers.Timer? _schedulerTimer; public ScriptScheduler(IServiceProvider serviceProvider) @@ -40,7 +40,7 @@ public void Dispose() public Task StartAsync(CancellationToken cancellationToken) { _schedulerTimer?.Dispose(); - _schedulerTimer = new System.Timers.Timer(_timerInterval.TotalMilliseconds); + _schedulerTimer = new System.Timers.Timer(_timerInterval); _schedulerTimer.Elapsed += SchedulerTimer_Elapsed; _schedulerTimer.Start(); return Task.CompletedTask; @@ -52,7 +52,7 @@ public Task StopAsync(CancellationToken cancellationToken) return Task.CompletedTask; } - private void SchedulerTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) + private void SchedulerTimer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e) { _ = DispatchScriptRuns(); } @@ -63,13 +63,14 @@ public async Task DispatchScriptRuns() var scriptScheduleDispatcher = scope.ServiceProvider.GetRequiredService(); var logger = scope.ServiceProvider.GetRequiredService>(); + if (!await _dispatchLock.WaitAsync(0)) + { + logger.LogWarning("Script schedule dispatcher is already running. Returning."); + return; + } + try { - if (!await _dispatchLock.WaitAsync(0)) - { - logger.LogWarning("Script schedule dispatcher is already running. Returning."); - return; - } await scriptScheduleDispatcher.DispatchPendingScriptRuns(); } diff --git a/Server/Services/ToastService.cs b/Server/Services/ToastService.cs index de943060e..a42af683b 100644 --- a/Server/Services/ToastService.cs +++ b/Server/Services/ToastService.cs @@ -14,25 +14,25 @@ public interface IToastService void ShowToast( string message, int expirationMillisecond = 3000, - string classString = null, - string styleOverrides = null); + string classString = "", + string styleOverrides = ""); void ShowToast2( string message, ToastType toastType = ToastType.Info, int expirationMillisecond = 3000, - string styleOverrides = null); + string styleOverrides = ""); } public class ToastService : IToastService { - public event EventHandler OnToastsChanged; + public event EventHandler? OnToastsChanged; public ConcurrentList Toasts { get; } = new(); public void ShowToast(string message, int expirationMillisecond = 3000, - string classString = null, - string styleOverrides = null) + string classString = "", + string styleOverrides = "") { if (string.IsNullOrWhiteSpace(classString)) @@ -57,7 +57,7 @@ public void ShowToast(string message, removeToastTimer.Elapsed += (s, e) => { Toasts.Remove(toastModel); - OnToastsChanged?.Invoke(this, null); + OnToastsChanged?.Invoke(this, EventArgs.Empty); removeToastTimer.Dispose(); }; removeToastTimer.Start(); @@ -67,7 +67,7 @@ public void ShowToast2( string message, ToastType toastType, int expirationMillisecond = 3000, - string styleOverrides = null) + string styleOverrides = "") { var classString = toastType switch { diff --git a/Server/Services/UpgradeService.cs b/Server/Services/UpgradeService.cs index 333c9e591..81eb2747e 100644 --- a/Server/Services/UpgradeService.cs +++ b/Server/Services/UpgradeService.cs @@ -20,7 +20,7 @@ public class UpgradeService : IUpgradeService { private readonly IHttpClientFactory _httpClientFactory; private readonly ILogger _logger; - private Version _currentVersion; + private Version? _currentVersion; public UpgradeService(IHttpClientFactory httpClientFactory, ILogger logger) { @@ -59,10 +59,22 @@ public async Task IsNewVersionAvailable() { using var client = _httpClientFactory.CreateClient(); var response = await client.GetAsync("https://github.com/immense/Remotely/releases/latest"); - var versionString = response.RequestMessage.RequestUri.ToString().Split("/").Last()[1..]; + var versionTag = $"{response.RequestMessage?.RequestUri}".Split("/").LastOrDefault(); + if (string.IsNullOrWhiteSpace(versionTag)) + { + return false; + } + var versionString = versionTag[1..]; var remoteVersion = Version.Parse(versionString); + var filePath = Directory.GetFiles(Directory.GetCurrentDirectory(), "Remotely_Server.dll", SearchOption.AllDirectories).First(); - var localVersion = Version.Parse(System.Diagnostics.FileVersionInfo.GetVersionInfo(filePath).FileVersion); + var fileVersion = System.Diagnostics.FileVersionInfo.GetVersionInfo(filePath).FileVersion; + if (string.IsNullOrWhiteSpace(fileVersion)) + { + return false; + } + var localVersion = Version.Parse(fileVersion); + if (remoteVersion > localVersion) { return true; diff --git a/Shared/Models/ConnectionInfo.cs b/Shared/Models/ConnectionInfo.cs index 17165ac35..e5292b888 100644 --- a/Shared/Models/ConnectionInfo.cs +++ b/Shared/Models/ConnectionInfo.cs @@ -16,7 +16,7 @@ public string? Host } set { - _host = value?.Trim()?.TrimEnd('/') ?? string.Empty; + _host = value?.Trim()?.TrimEnd('/'); } } public string? OrganizationID { get; set; } diff --git a/Shared/Models/Device.cs b/Shared/Models/Device.cs index 865aed44c..3720bad53 100644 --- a/Shared/Models/Device.cs +++ b/Shared/Models/Device.cs @@ -11,7 +11,6 @@ namespace Remotely.Shared.Models; public class Device { - public static Device Empty { get; } = new(); [Sortable] [Display(Name = "Agent Version")] diff --git a/Shared/Models/PwshCommandCompletion.cs b/Shared/Models/PwshCommandCompletion.cs index b9fddf06f..6c71fdaf2 100644 --- a/Shared/Models/PwshCommandCompletion.cs +++ b/Shared/Models/PwshCommandCompletion.cs @@ -11,7 +11,7 @@ public class PwshCommandCompletion public int CurrentMatchIndex { get; set; } public int ReplacementIndex { get; set; } public int ReplacementLength { get; set; } - public List CompletionMatches { get; set; } + public List CompletionMatches { get; set; } = new(); } @@ -26,13 +26,13 @@ public PwshCompletionResult(string completionText, string listItemText, PwshComp ToolTip = toolTip; } - public string CompletionText { get; set; } + public string CompletionText { get; set; } = string.Empty; - public string ListItemText { get; set; } + public string ListItemText { get; set; } = string.Empty; public PwshCompletionResultType ResultType { get; set; } - public string ToolTip { get; set; } + public string ToolTip { get; set; } = string.Empty; } public enum PwshCompletionResultType diff --git a/Shared/Models/RemotelyUserOptions.cs b/Shared/Models/RemotelyUserOptions.cs index 6d128477a..56bd6d103 100644 --- a/Shared/Models/RemotelyUserOptions.cs +++ b/Shared/Models/RemotelyUserOptions.cs @@ -7,7 +7,7 @@ public class RemotelyUserOptions { [Display(Name = "Display Name")] [StringLength(100)] - public string DisplayName { get; set; } + public string? DisplayName { get; set; } [Display(Name = "PS Core Shortcut")] [StringLength(10)] diff --git a/Shared/Services/ProcessInvoker.cs b/Shared/Services/ProcessInvoker.cs index 58ede284b..23e83d615 100644 --- a/Shared/Services/ProcessInvoker.cs +++ b/Shared/Services/ProcessInvoker.cs @@ -32,9 +32,9 @@ public string InvokeProcessOutput(string command, string arguments) }; var proc = Process.Start(psi); - proc.WaitForExit(); + proc?.WaitForExit(); - return proc.StandardOutput.ReadToEnd(); + return proc?.StandardOutput.ReadToEnd() ?? string.Empty; } catch (Exception ex) { diff --git a/Shared/Utilities/AppVersionHelper.cs b/Shared/Utilities/AppVersionHelper.cs index bcd3ce3c7..1ffb1c964 100644 --- a/Shared/Utilities/AppVersionHelper.cs +++ b/Shared/Utilities/AppVersionHelper.cs @@ -34,7 +34,7 @@ public static string GetAppVersion(string defaultVersion = "1.0.0") } } - private static bool TryGetFileVersion(string filePath, out string version) + private static bool TryGetFileVersion(string? filePath, out string version) { try { diff --git a/Shared/Utilities/ConsoleHelper.cs b/Shared/Utilities/ConsoleHelper.cs index 5d4da78b5..ee7b8750b 100644 --- a/Shared/Utilities/ConsoleHelper.cs +++ b/Shared/Utilities/ConsoleHelper.cs @@ -28,7 +28,7 @@ public static string GetSelection(string promptMessage, params string[] options) Console.Write("Enter Response: "); Console.ForegroundColor = ConsoleColor.Gray; - return Console.ReadLine().Trim(); + return Console.ReadLine()?.Trim() ?? string.Empty; } public static string ReadLine(string prompt, ConsoleColor promptColor = ConsoleColor.Cyan, string subprompt = "") @@ -50,7 +50,7 @@ public static string ReadLine(string prompt, ConsoleColor promptColor = ConsoleC var response = Console.ReadLine(); Console.WriteLine(); - return response; + return response ?? string.Empty; } public static bool TryParseBoolLike(string value, out bool result) diff --git a/Shared/Utilities/Logger.cs b/Shared/Utilities/Logger.cs index 5ab3f5125..b56087f31 100644 --- a/Shared/Utilities/Logger.cs +++ b/Shared/Utilities/Logger.cs @@ -15,7 +15,7 @@ namespace Remotely.Shared.Utilities; [Obsolete("Please use ILogger via dependency injection.")] public static class Logger { - private static string _logDir; + private static string? _logDir; private static string LogDir { @@ -130,7 +130,7 @@ public static void Write(Exception ex, EventType eventType = EventType.Error, [C while (exception != null) { - File.AppendAllText(LogPath, $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}\t[{eventType}]\t[{callerName}]\t{exception?.Message}\t{exception?.StackTrace}\t{exception?.Source}{Environment.NewLine}"); + File.AppendAllText(LogPath, $"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}\t[{eventType}]\t[{callerName}]\t{exception.Message}\t{exception.StackTrace}\t{exception.Source}{Environment.NewLine}"); Console.WriteLine(exception.Message); exception = exception.InnerException; } diff --git a/Shared/ViewModels/ChatHistoryItem.cs b/Shared/ViewModels/ChatHistoryItem.cs index 8dd32afab..9b5db9cc9 100644 --- a/Shared/ViewModels/ChatHistoryItem.cs +++ b/Shared/ViewModels/ChatHistoryItem.cs @@ -10,7 +10,7 @@ namespace Remotely.Shared.ViewModels; public class ChatHistoryItem { public ChatHistoryItemOrigin Origin { get; init; } - public string Message { get; init; } + public string? Message { get; init; } public DateTimeOffset Timestamp { get; init; } = DateTime.Now; diff --git a/Shared/ViewModels/ChatSession.cs b/Shared/ViewModels/ChatSession.cs index 4bf5c2dc1..3d7a30f8c 100644 --- a/Shared/ViewModels/ChatSession.cs +++ b/Shared/ViewModels/ChatSession.cs @@ -6,8 +6,8 @@ namespace Remotely.Shared.ViewModels; public class ChatSession { public ConcurrentList ChatHistory { get; } = new(); - public string DeviceId { get; set; } - public string DeviceName { get; set; } + public string? DeviceId { get; set; } + public string? DeviceName { get; set; } public string ExpandedClass => IsExpanded ? "expanded" : ""; public bool IsExpanded { get; set; } diff --git a/Shared/ViewModels/InviteViewModel.cs b/Shared/ViewModels/InviteViewModel.cs index 4d59b5e47..96eef5b4f 100644 --- a/Shared/ViewModels/InviteViewModel.cs +++ b/Shared/ViewModels/InviteViewModel.cs @@ -5,9 +5,9 @@ namespace Remotely.Shared.ViewModels; public class InviteViewModel { - public string ID { get; set; } + public string? ID { get; set; } public bool IsAdmin { get; set; } public DateTimeOffset DateSent { get; set; } [EmailAddress] - public string InvitedUser { get; set; } + public string? InvitedUser { get; set; } } diff --git a/Shared/ViewModels/OrganizationUser.cs b/Shared/ViewModels/OrganizationUser.cs index 537a78d4e..860604bf7 100644 --- a/Shared/ViewModels/OrganizationUser.cs +++ b/Shared/ViewModels/OrganizationUser.cs @@ -2,7 +2,7 @@ public class OrganizationUser { - public string ID { get; set; } - public string UserName { get; set; } + public string? ID { get; set; } + public string? UserName { get; set; } public bool IsAdmin { get; set; } } diff --git a/Shared/ViewModels/TerminalLineItem.cs b/Shared/ViewModels/TerminalLineItem.cs index 00751df29..bcef221da 100644 --- a/Shared/ViewModels/TerminalLineItem.cs +++ b/Shared/ViewModels/TerminalLineItem.cs @@ -10,7 +10,7 @@ namespace Remotely.Shared.ViewModels; public class TerminalLineItem { public Guid Id { get; } = Guid.NewGuid(); - public string Text { get; set; } - public string ClassName { get; set; } - public string Title { get; set; } + public string? Text { get; set; } + public string? ClassName { get; set; } + public string? Title { get; set; } } diff --git a/Tests/LoadTester/CommandLineParser.cs b/Tests/LoadTester/CommandLineParser.cs index ce2e5c6f7..0cb914b29 100644 --- a/Tests/LoadTester/CommandLineParser.cs +++ b/Tests/LoadTester/CommandLineParser.cs @@ -1,24 +1,32 @@ using System; +using System.Linq; using System.Collections.Generic; namespace Remotely.Tests.LoadTester; public class CommandLineParser { - private static Dictionary commandLineArgs; + private static Dictionary? _commandLineArgs; + public static Dictionary CommandLineArgs { get { - if (commandLineArgs is null) + if (_commandLineArgs is null) { - commandLineArgs = new Dictionary(); + _commandLineArgs = new Dictionary(); + var args = Environment.GetCommandLineArgs(); + if (args?.Any() != true) + { + return _commandLineArgs; + } + for (var i = 1; i < args.Length; i += 2) { try { - var key = args?[i]; + var key = args[i]; if (key != null) { if (!key.Contains("-")) @@ -29,14 +37,14 @@ public static Dictionary CommandLineArgs key = key.Trim().Replace("-", "").ToLower(); - commandLineArgs.Add(key, args[i + 1]); + _commandLineArgs.Add(key, args[i + 1]); } } catch { } } } - return commandLineArgs; + return _commandLineArgs; } } } diff --git a/Tests/LoadTester/Program.cs b/Tests/LoadTester/Program.cs index 3b341c0c0..00d078cdb 100644 --- a/Tests/LoadTester/Program.cs +++ b/Tests/LoadTester/Program.cs @@ -17,12 +17,12 @@ internal class Program { private static readonly double _heartbeatMs = TimeSpan.FromMinutes(1).TotalMilliseconds; private static int _agentCount; - private static string _organizationId; - private static string _serverurl; - private static Mock _cpuSampler; - private static Mock> _logger; - private static DeviceInfoGeneratorWin _deviceInfo; - private static Stopwatch _stopwatch; + private static string? _organizationId; + private static string? _serverurl; + private static Mock? _cpuSampler; + private static Mock>? _logger; + private static DeviceInfoGeneratorWin? _deviceInfo; + private static Stopwatch? _stopwatch; private static int _connectedCount; private static void Main(string[] args) @@ -96,7 +96,7 @@ private static async Task StartAgent(int i) Console.WriteLine($"Connecting device number {i}"); await hubConnection.StartAsync(); - var device = await _deviceInfo.CreateDevice(deviceId, _organizationId); + var device = await _deviceInfo!.CreateDevice(deviceId, _organizationId!); device.DeviceName = "TestDevice-" + Guid.NewGuid(); var result = await hubConnection.InvokeAsync("DeviceCameOnline", device); @@ -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); } @@ -130,7 +130,7 @@ private static async Task StartAgent(int i) Interlocked.Increment(ref _connectedCount); if (_connectedCount == _agentCount) { - Console.WriteLine($"Finished connecting all devices. Elapsed: {_stopwatch.Elapsed}"); + Console.WriteLine($"Finished connecting all devices. Elapsed: {_stopwatch!.Elapsed}"); } break; diff --git a/Tests/Server.Tests/AgentHubTests.cs b/Tests/Server.Tests/AgentHubTests.cs index 94929bae5..e5a7283bf 100644 --- a/Tests/Server.Tests/AgentHubTests.cs +++ b/Tests/Server.Tests/AgentHubTests.cs @@ -24,9 +24,8 @@ namespace Remotely.Tests; [TestClass] public class AgentHubTests { - private TestData _testData; - - public IDataService DataService { get; private set; } + private TestData _testData = null!; + private IDataService _dataService = null!; [TestMethod] [DoNotParallelize] @@ -42,10 +41,10 @@ public async Task DeviceCameOnline_BannedByName() var serviceSessionCache = new Mock(); var logger = new Mock>(); - appConfig.Setup(x => x.BannedDevices).Returns(new string[] { _testData.Org1Device1.DeviceName }); + appConfig.Setup(x => x.BannedDevices).Returns(new string[] { $"{_testData.Org1Device1.DeviceName}" }); var hub = new AgentHub( - DataService, + _dataService, appConfig.Object, serviceSessionCache.Object, viewerHub.Object, @@ -82,7 +81,7 @@ public async Task DeviceCameOnline_BannedById() appConfig.Setup(x => x.BannedDevices).Returns(new string[] { _testData.Org1Device1.ID }); var hub = new AgentHub( - DataService, + _dataService, appConfig.Object, serviceSessionCache.Object, viewerHub.Object, @@ -111,18 +110,17 @@ public async Task TestInit() { _testData = new TestData(); await _testData.Init(); - DataService = IoCActivator.ServiceProvider.GetRequiredService(); + _dataService = IoCActivator.ServiceProvider.GetRequiredService(); } private class CallerContext : HubCallerContext { public override string ConnectionId => "test-id"; - public override string UserIdentifier => null; - - public override ClaimsPrincipal User => null; + public override string? UserIdentifier => null; + public override ClaimsPrincipal? User => null; - public override IDictionary Items { get; } = new Dictionary(); + public override IDictionary Items { get; } = new Dictionary(); public override IFeatureCollection Features { get; } = new FeatureCollection(); diff --git a/Tests/Server.Tests/CircuitConnectionTests.cs b/Tests/Server.Tests/CircuitConnectionTests.cs index b9da16089..37a104085 100644 --- a/Tests/Server.Tests/CircuitConnectionTests.cs +++ b/Tests/Server.Tests/CircuitConnectionTests.cs @@ -133,7 +133,7 @@ public async Task WakeDevice_GivenMatchingPeerByIp_UsesCorrectPeer() var addToGroupResult = _dataService.AddUserToDeviceGroup( _testData.Org1Id, _testData.Org1Group1.ID, - _testData.Org1User1.UserName, + $"{_testData.Org1User1.UserName}", out _); var updateResult = await _dataService.AddOrUpdateDevice(_testData.Org1Device1.ToDto()); @@ -199,7 +199,7 @@ public async Task WakeDevice_GivenMatchingPeerByGroupId_UsesCorrectPeer() var addToGroupResult = _dataService.AddUserToDeviceGroup( _testData.Org1Id, _testData.Org1Group1.ID, - _testData.Org1User1.UserName, + $"{_testData.Org1User1.UserName}", out _); var updateResult = await _dataService.AddOrUpdateDevice(_testData.Org1Device1.ToDto()); @@ -278,7 +278,7 @@ public async Task WakeDevice_GivenNoMatchingGroupOrIp_DoesNotSend() var addToGroupResult = _dataService.AddUserToDeviceGroup( _testData.Org1Id, _testData.Org1Group1.ID, - _testData.Org1User1.UserName, + $"{_testData.Org1User1.UserName}", out _); var updateResult = await _dataService.AddOrUpdateDevice(_testData.Org1Device1.ToDto()); @@ -346,7 +346,7 @@ public async Task WakeDevices_GivenPeerIpMatches_UsesCorrectPeer() var addToGroupResult = _dataService.AddUserToDeviceGroup( _testData.Org1Id, _testData.Org1Group1.ID, - _testData.Org1User1.UserName, + $"{_testData.Org1User1.UserName}", out _); var updateResult = await _dataService.AddOrUpdateDevice(_testData.Org1Device1.ToDto()); @@ -417,7 +417,7 @@ public async Task WakeDevices_GivenMatchingPeerByGroupId_UsesCorrectPeer() var addToGroupResult = _dataService.AddUserToDeviceGroup( _testData.Org1Id, _testData.Org1Group1.ID, - _testData.Org1User1.UserName, + $"{_testData.Org1User1.UserName}", out _); var updateResult = await _dataService.AddOrUpdateDevice(_testData.Org1Device1.ToDto()); diff --git a/Tests/Server.Tests/IoCActivator.cs b/Tests/Server.Tests/IoCActivator.cs index 19d050745..7c0e84f65 100644 --- a/Tests/Server.Tests/IoCActivator.cs +++ b/Tests/Server.Tests/IoCActivator.cs @@ -21,25 +21,25 @@ namespace Remotely.Tests; [TestClass] public class IoCActivator { - public static IServiceProvider ServiceProvider { get; set; } - private static IWebHostBuilder builder; + public static IServiceProvider ServiceProvider { get; set; } = null!; + private static IWebHostBuilder? _builder; public static void Activate() { - if (builder is null) + if (_builder is null) { - builder = WebHost.CreateDefaultBuilder() + _builder = WebHost.CreateDefaultBuilder() .UseStartup() .CaptureStartupErrors(true) .ConfigureAppConfiguration(config => { - config.AddInMemoryCollection(new Dictionary() + config.AddInMemoryCollection(new Dictionary() { ["ApplicationOptions:DBProvider"] = "InMemory" }); }); - builder.Build(); + _builder.Build(); } } diff --git a/Tests/Server.Tests/ScriptScheduleDispatcherTests.cs b/Tests/Server.Tests/ScriptScheduleDispatcherTests.cs index c55234fa2..be1d0f926 100644 --- a/Tests/Server.Tests/ScriptScheduleDispatcherTests.cs +++ b/Tests/Server.Tests/ScriptScheduleDispatcherTests.cs @@ -19,14 +19,14 @@ namespace Remotely.Tests; [TestClass] public class ScriptScheduleDispatcherTests { - private ScriptSchedule _schedule1; - private Mock _dataService; - private Mock _circuitConnection; - private Mock _serviceSessionCache; - private Mock> _logger; - private ScriptScheduleDispatcher _dispatcher; - private TestData _testData; - private SavedScript _savedScript; + private ScriptSchedule _schedule1 = null!; + private Mock _dataService = null!; + private Mock _circuitConnection = null!; + private Mock _serviceSessionCache = null!; + private Mock> _logger = null!; + private ScriptScheduleDispatcher _dispatcher = null!; + private TestData _testData = null!; + private SavedScript _savedScript = null!; [TestInitialize] public async Task Init() @@ -112,8 +112,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.Org1Device1.ID) && - x.Devices.Exists(d => d.ID == _testData.Org1Device2.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( From 95ebedf89e48731b9d9f0839ada13055f0a49d66 Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Wed, 26 Jul 2023 12:07:14 -0700 Subject: [PATCH 07/29] Update Immense.RemoteControl --- submodules/Immense.RemoteControl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/Immense.RemoteControl b/submodules/Immense.RemoteControl index 4de148bc4..0c08b0028 160000 --- a/submodules/Immense.RemoteControl +++ b/submodules/Immense.RemoteControl @@ -1 +1 @@ -Subproject commit 4de148bc4f37bf625186155373dd176e6c492207 +Subproject commit 0c08b00282ea01d358ad51a20693e8ec4fca2071 From e70d083a3fb9d4a6dcc6809394fabc0174358482 Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Wed, 26 Jul 2023 12:52:15 -0700 Subject: [PATCH 08/29] Don't send the whole script result back from the API. --- Agent/Services/ScriptExecutor.cs | 10 +++++----- Server/API/ScriptResultsController.cs | 7 +++++-- Shared/Models/ScriptResultResponse.cs | 11 +++++++++++ 3 files changed, 21 insertions(+), 7 deletions(-) create mode 100644 Shared/Models/ScriptResultResponse.cs diff --git a/Agent/Services/ScriptExecutor.cs b/Agent/Services/ScriptExecutor.cs index 1e983fd8c..ccd3bc36a 100644 --- a/Agent/Services/ScriptExecutor.cs +++ b/Agent/Services/ScriptExecutor.cs @@ -48,7 +48,7 @@ public async Task RunCommandFromApi(ScriptingShell shell, result.InputType = ScriptInputType.Api; result.SenderUserName = senderUsername; - await SendResultsToApi(result, authToken); + _ = await SendResultsToApi(result, authToken); await hubConnection.SendAsync("ScriptResultViaApi", requestID); } catch (Exception ex) @@ -78,7 +78,7 @@ public async Task RunCommandFromTerminal(ScriptingShell shell, { return; } - await hubConnection.SendAsync("ScriptResult", responseResult.ID); + await hubConnection.SendAsync("ScriptResult", responseResult.Id); } catch (Exception ex) { @@ -143,7 +143,7 @@ public async Task RunScript(Guid savedScriptId, result.InputType = scriptInputType; result.SavedScriptId = savedScriptId; - var responseResult = await SendResultsToApi(result, expiringToken); + _ = await SendResultsToApi(result, expiringToken); } catch (Exception ex) { @@ -196,7 +196,7 @@ private async Task ExecuteScriptContent( } throw new InvalidOperationException($"Unknown shell type: {shell}"); } - private async Task SendResultsToApi(object result, string expiringToken) + private async Task SendResultsToApi(object result, string expiringToken) { var targetURL = _configService.GetConnectionInfo().Host + $"/API/ScriptResults"; @@ -212,6 +212,6 @@ private async Task ExecuteScriptContent( } var content = await response.Content.ReadAsStringAsync(); - return JsonSerializer.Deserialize(content, JsonSerializerHelper.CaseInsensitiveOptions); + return JsonSerializer.Deserialize(content, JsonSerializerHelper.CaseInsensitiveOptions); } } \ No newline at end of file diff --git a/Server/API/ScriptResultsController.cs b/Server/API/ScriptResultsController.cs index b47a5eb7b..8936763fe 100644 --- a/Server/API/ScriptResultsController.cs +++ b/Server/API/ScriptResultsController.cs @@ -54,7 +54,7 @@ public ActionResult DownloadResults(string scriptId) [HttpPost] [ServiceFilter(typeof(ExpiringTokenFilter))] - public async Task> Post([FromBody] ScriptResult result) + public async Task> Post([FromBody] ScriptResult result) { _dataService.AddOrUpdateScriptResult(result); @@ -111,6 +111,9 @@ await _emailSender.SendEmailAsync(savedScript.SendErrorEmailTo, await _dataService.AddScriptResultToScriptRun(result.ID, result.ScriptRunId.Value); } - return result; + return new ScriptResultResponse() + { + Id = result.ID + }; } } diff --git a/Shared/Models/ScriptResultResponse.cs b/Shared/Models/ScriptResultResponse.cs new file mode 100644 index 000000000..ea6ceaddf --- /dev/null +++ b/Shared/Models/ScriptResultResponse.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Remotely.Shared.Models; +public class ScriptResultResponse +{ + public required string Id { get; init; } +} From cf0fad5c424ceace07f3a11de939f21af25c441c Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Wed, 26 Jul 2023 12:55:11 -0700 Subject: [PATCH 09/29] Add missing type on SendResultsToApi. --- Agent/Services/ScriptExecutor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Agent/Services/ScriptExecutor.cs b/Agent/Services/ScriptExecutor.cs index ccd3bc36a..262e7cf47 100644 --- a/Agent/Services/ScriptExecutor.cs +++ b/Agent/Services/ScriptExecutor.cs @@ -196,7 +196,7 @@ private async Task ExecuteScriptContent( } throw new InvalidOperationException($"Unknown shell type: {shell}"); } - private async Task SendResultsToApi(object result, string expiringToken) + private async Task SendResultsToApi(ScriptResult result, string expiringToken) { var targetURL = _configService.GetConnectionInfo().Host + $"/API/ScriptResults"; From 459c63ffbedbda71e884ff11cdc4d92bc8c4ca26 Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Wed, 26 Jul 2023 13:01:58 -0700 Subject: [PATCH 10/29] Update ScriptRun/ScriptResult relationship. --- Server/Data/AppDb.cs | 10 +++++++++- Shared/Models/ScriptResult.cs | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Server/Data/AppDb.cs b/Server/Data/AppDb.cs index 9a3deff42..a308ce8f9 100644 --- a/Server/Data/AppDb.cs +++ b/Server/Data/AppDb.cs @@ -41,7 +41,7 @@ public class AppDb : IdentityDbContext protected override void OnConfiguring(DbContextOptionsBuilder options) { options.ConfigureWarnings(x => x.Ignore(RelationalEventId.MultipleCollectionIncludeWarning)); - //options.LogTo((message) => System.Diagnostics.Debug.Write(message)); + options.LogTo((message) => System.Diagnostics.Debug.Write(message)); } protected override void OnModelCreating(ModelBuilder builder) @@ -157,6 +157,10 @@ protected override void OnModelCreating(ModelBuilder builder) builder.Entity() .HasMany(x => x.Devices) .WithMany(x => x.ScriptRuns); + builder.Entity() + .HasMany(x => x.Results) + .WithOne(x => x.ScriptRun) + .IsRequired(false); builder.Entity() .Property(x => x.ErrorOutput) @@ -173,6 +177,10 @@ protected override void OnModelCreating(ModelBuilder builder) x => DeserializeStringArray(x, jsonOptions)) .Metadata .SetValueComparer(_stringArrayComparer); + builder.Entity() + .HasOne(x => x.ScriptRun) + .WithMany(x => x.Results) + .IsRequired(false); builder.Entity() .HasOne(x => x.User) diff --git a/Shared/Models/ScriptResult.cs b/Shared/Models/ScriptResult.cs index 8d6844260..440c2f1e8 100644 --- a/Shared/Models/ScriptResult.cs +++ b/Shared/Models/ScriptResult.cs @@ -43,6 +43,7 @@ public class ScriptResult public int? ScheduleId { get; set; } public Guid? SavedScriptId { get; set; } + public ScriptRun? ScriptRun { get; set; } public int? ScriptRunId { get; set; } From cb220f2ea03137b5364fe0b71a87db4dc0609da7 Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Wed, 26 Jul 2023 14:07:40 -0700 Subject: [PATCH 11/29] Remove test strings. --- Server/Components/Scripts/SavedScripts.razor.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Server/Components/Scripts/SavedScripts.razor.cs b/Server/Components/Scripts/SavedScripts.razor.cs index a9e4b5e05..1cfc96755 100644 --- a/Server/Components/Scripts/SavedScripts.razor.cs +++ b/Server/Components/Scripts/SavedScripts.razor.cs @@ -19,7 +19,7 @@ public partial class SavedScripts : AuthComponentBase [CascadingParameter] private ScriptsPage ParentPage { get; set; } = null!; - private SavedScript _selectedScript = new() { Name = "Test Script" }; + private SavedScript _selectedScript = new() { Name = string.Empty }; private string _alertMessage = string.Empty; private string _alertOptionsShowClass = string.Empty; private string _environmentVarsShowClass = string.Empty; @@ -74,7 +74,7 @@ private void CreateNew() { _selectedScript = new() { - Name = "Test Script" + Name = string.Empty }; } @@ -95,7 +95,7 @@ private async Task DeleteSelectedScript() await ParentPage.RefreshScripts(); _selectedScript = new() { - Name = "Test Script" + Name = string.Empty }; } } From 7a8809ca904e44ec88c490569deecdbdfa348051 Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Wed, 26 Jul 2023 14:07:54 -0700 Subject: [PATCH 12/29] Add AsNoTracking to read-only queries. --- Server/Services/DataService.cs | 109 +++++++++++++++++++++++++-------- 1 file changed, 82 insertions(+), 27 deletions(-) diff --git a/Server/Services/DataService.cs b/Server/Services/DataService.cs index 8e3ef98b3..758d3088e 100644 --- a/Server/Services/DataService.cs +++ b/Server/Services/DataService.cs @@ -434,9 +434,9 @@ public async Task AddOrUpdateSavedScript(SavedScript script, string user } dbContext.SavedScripts.Update(script); - script.CreatorId = userId; + script.CreatorId = user.Id; script.Creator = user; - script.OrganizationID = user.Id; + script.OrganizationID = user.OrganizationID; await dbContext.SaveChangesAsync(); return Result.Ok(); } @@ -1062,6 +1062,7 @@ public async Task> GetAlert(string alertId) using var dbContext = _appDbFactory.GetContext(); var alert = await dbContext.Alerts + .AsNoTracking() .Include(x => x.Device) .Include(x => x.User) .FirstOrDefaultAsync(x => x.ID == alertId); @@ -1079,6 +1080,7 @@ public Alert[] GetAlerts(string userId) using var dbContext = _appDbFactory.GetContext(); return dbContext.Alerts + .AsNoTracking() .Include(x => x.Device) .Include(x => x.User) .Where(x => x.UserID == userId) @@ -1098,6 +1100,7 @@ public ApiToken[] GetAllApiTokens(string userId) } return dbContext.ApiTokens + .AsNoTracking() .Where(x => x.OrganizationID == user.OrganizationID) .OrderByDescending(x => x.LastUsed) .ToArray(); @@ -1108,6 +1111,7 @@ public ScriptResult[] GetAllCommandResults(string orgId) using var dbContext = _appDbFactory.GetContext(); return dbContext.ScriptResults + .AsNoTracking() .Where(x => x.OrganizationID == orgId) .OrderByDescending(x => x.TimeStamp) .ToArray(); @@ -1118,6 +1122,7 @@ public ScriptResult[] GetAllCommandResultsForUser(string orgId, string userName, using var dbContext = _appDbFactory.GetContext(); return dbContext.ScriptResults + .AsNoTracking() .Where(x => x.OrganizationID == orgId && x.SenderUserName == userName && x.DeviceID == deviceId) @@ -1129,7 +1134,10 @@ public Device[] GetAllDevices(string orgId) { using var dbContext = _appDbFactory.GetContext(); - return dbContext.Devices.Where(x => x.OrganizationID == orgId).ToArray(); + return dbContext.Devices + .AsNoTracking() + .Where(x => x.OrganizationID == orgId) + .ToArray(); } public InviteLink[] GetAllInviteLinks(string organizationId) @@ -1137,6 +1145,7 @@ public InviteLink[] GetAllInviteLinks(string organizationId) using var dbContext = _appDbFactory.GetContext(); return dbContext.InviteLinks + .AsNoTracking() .Where(x => x.OrganizationID == organizationId) .ToArray(); } @@ -1146,6 +1155,7 @@ public ScriptResult[] GetAllScriptResults(string orgId, string deviceId) using var dbContext = _appDbFactory.GetContext(); return dbContext.ScriptResults + .AsNoTracking() .Where(x => x.OrganizationID == orgId && x.DeviceID == deviceId) .OrderByDescending(x => x.TimeStamp) .ToArray(); @@ -1156,6 +1166,7 @@ public ScriptResult[] GetAllScriptResultsForUser(string orgId, string userName) using var dbContext = _appDbFactory.GetContext(); return dbContext.ScriptResults + .AsNoTracking() .Where(x => x.OrganizationID == orgId && x.SenderUserName == userName) .OrderByDescending(x => x.TimeStamp) .ToArray(); @@ -1165,7 +1176,9 @@ public RemotelyUser[] GetAllUsersForServer() { using var dbContext = _appDbFactory.GetContext(); - return dbContext.Users.ToArray(); + return dbContext.Users + .AsNoTracking() + .ToArray(); } public async Task GetAllUsersInOrganization(string orgId) @@ -1178,6 +1191,7 @@ public async Task GetAllUsersInOrganization(string orgId) using var dbContext = _appDbFactory.GetContext(); var organization = await dbContext.Organizations + .AsNoTracking() .Include(x => x.RemotelyUsers) .FirstOrDefaultAsync(x => x.ID == orgId); @@ -1198,7 +1212,9 @@ public async Task> GetApiKey(string keyId) using var dbContext = _appDbFactory.GetContext(); - var token = await dbContext.ApiTokens.FirstOrDefaultAsync(x => x.ID == keyId); + var token = await dbContext.ApiTokens + .AsNoTracking() + .FirstOrDefaultAsync(x => x.ID == keyId); if (token is null) { @@ -1218,6 +1234,7 @@ public async Task> GetBrandingInfo(string organizationId) using var dbContext = _appDbFactory.GetContext(); var organization = await dbContext.Organizations + .AsNoTracking() .Include(x => x.BrandingInfo) .FirstOrDefaultAsync(x => x.ID == organizationId); @@ -1245,7 +1262,9 @@ public async Task> GetDefaultOrganization() { using var dbContext = _appDbFactory.GetContext(); - var org = await dbContext.Organizations.FirstOrDefaultAsync(x => x.IsDefaultOrganization); + var org = await dbContext.Organizations + .AsNoTracking() + .FirstOrDefaultAsync(x => x.IsDefaultOrganization); if (org is null) { @@ -1259,9 +1278,11 @@ public async Task> GetDevice(string orgId, string deviceId) { using var dbContext = _appDbFactory.GetContext(); - var device = await dbContext.Devices.FirstOrDefaultAsync(x => - x.OrganizationID == orgId && - x.ID == deviceId); + var device = await dbContext.Devices + .AsNoTracking() + .FirstOrDefaultAsync(x => + x.OrganizationID == orgId && + x.ID == deviceId); if (device is null) { @@ -1276,8 +1297,12 @@ public async Task> GetDevice( { using var dbContext = _appDbFactory.GetContext(); - var query = dbContext.Devices.AsQueryable(); + var query = dbContext.Devices + .AsNoTracking() + .AsQueryable(); + includesBuilder?.Invoke(query); + var device = await query.FirstOrDefaultAsync(x => x.ID == deviceId); if (device is null) @@ -1304,6 +1329,7 @@ public int GetDeviceCount(RemotelyUser user) } return dbContext.Users + .AsNoTracking() .Include(x => x.DeviceGroups) .ThenInclude(x => x.Devices) .Where(x => x.Id == user.Id) @@ -1319,7 +1345,9 @@ public async Task> GetDeviceGroup( { using var dbContext = _appDbFactory.GetContext(); - var query = dbContext.DeviceGroups.AsQueryable(); + var query = dbContext.DeviceGroups + .AsNoTracking() + .AsQueryable(); if (includeDevices) { @@ -1343,7 +1371,9 @@ public DeviceGroup[] GetDeviceGroups(string username) { using var dbContext = _appDbFactory.GetContext(); - var user = dbContext.Users.FirstOrDefault(x => x.UserName == username); + var user = dbContext.Users + .AsNoTracking() + .FirstOrDefault(x => x.UserName == username); if (user is null) { @@ -1352,6 +1382,7 @@ public DeviceGroup[] GetDeviceGroups(string username) var userId = user.Id; var groupIds = dbContext.DeviceGroups + .AsNoTracking() .Include(x => x.Users) .ThenInclude(x => x.DeviceGroups) .Where(x => @@ -1367,6 +1398,7 @@ public DeviceGroup[] GetDeviceGroups(string username) if (groupIds.Any()) { return dbContext.DeviceGroups + .AsNoTracking() .Where(x => groupIds.Contains(x.ID)) .OrderBy(x => x.Name) .ToArray(); @@ -1380,6 +1412,7 @@ public DeviceGroup[] GetDeviceGroupsForOrganization(string organizationId) using var dbContext = _appDbFactory.GetContext(); return dbContext.DeviceGroups + .AsNoTracking() .Include(x => x.Users) .ThenInclude(x => x.DeviceGroups) .Where(x => x.OrganizationID == organizationId) @@ -1392,6 +1425,7 @@ public List GetDevices(IEnumerable deviceIds) using var dbContext = _appDbFactory.GetContext(); return dbContext.Devices + .AsNoTracking() .Where(x => deviceIds.Contains(x.ID)) .ToList(); } @@ -1455,8 +1489,8 @@ public async Task> GetOrganizationByUserName(string userNam using var dbContext = _appDbFactory.GetContext(); - var user = await dbContext - .Users + var user = await dbContext.Users + .AsNoTracking() .Include(x => x.Organization) .FirstOrDefaultAsync(x => x.UserName!.ToLower() == userName.ToLower()); @@ -1486,7 +1520,9 @@ public async Task> GetOrganizationNameById(string organizationId) { using var dbContext = _appDbFactory.GetContext(); - var org = await dbContext.Organizations.FirstOrDefaultAsync(x => x.ID == organizationId); + var org = await dbContext.Organizations + .AsNoTracking() + .FirstOrDefaultAsync(x => x.ID == organizationId); if (org is null) { @@ -1506,8 +1542,9 @@ public async Task> GetOrganizationNameByUserName(string userName) using var dbContext = _appDbFactory.GetContext(); var user = await dbContext.Users - .Include(x => x.Organization) - .FirstOrDefaultAsync(x => x.UserName == userName); + .AsNoTracking() + .Include(x => x.Organization) + .FirstOrDefaultAsync(x => x.UserName == userName); if (user is null) { @@ -1523,6 +1560,7 @@ public async Task> GetPendingScriptRuns(string deviceId) using var dbContext = _appDbFactory.GetContext(); var device = await dbContext.Devices + .AsNoTracking() .Include(x => x.ScriptRuns) .ThenInclude(x => x.Results) .FirstOrDefaultAsync(x => x.ID == deviceId); @@ -1559,6 +1597,7 @@ public async Task> GetSavedScript(string userId, Guid script using var dbContext = _appDbFactory.GetContext(); var script = await dbContext.SavedScripts + .AsNoTracking() .Include(x => x.Creator) .FirstOrDefaultAsync(x => x.Id == scriptId && @@ -1574,7 +1613,9 @@ public async Task> GetSavedScript(string userId, Guid script public async Task> GetSavedScript(Guid scriptId) { using var dbContext = _appDbFactory.GetContext(); - var script = await dbContext.SavedScripts.FirstOrDefaultAsync(x => x.Id == scriptId); + var script = await dbContext.SavedScripts + .AsNoTracking() + .FirstOrDefaultAsync(x => x.Id == scriptId); if (script is null) { @@ -1588,6 +1629,7 @@ public async Task> GetSavedScriptsWithoutContent(string userId using var dbContext = _appDbFactory.GetContext(); return await dbContext.SavedScripts + .AsNoTracking() .Include(x => x.Creator) .Where(x => x.Creator!.OrganizationID == organizationId && @@ -1611,6 +1653,7 @@ public async Task> GetScriptResult(string resultId, string using var dbContext = _appDbFactory.GetContext(); var result = await dbContext.ScriptResults + .AsNoTracking() .FirstOrDefaultAsync(x => x.OrganizationID == orgId && x.ID == resultId); @@ -1639,6 +1682,7 @@ public async Task> GetScriptSchedules(string organizationId { using var dbContext = _appDbFactory.GetContext(); return await dbContext.ScriptSchedules + .AsNoTracking() .Include(x => x.Creator) .Include(x => x.Devices) .Include(x => x.DeviceGroups) @@ -1653,6 +1697,7 @@ public async Task> GetScriptSchedulesDue() var now = Time.Now; return await dbContext.ScriptSchedules + .AsNoTracking() .Include(x => x.Devices) .Include(x => x.DeviceGroups) .ThenInclude(x => x.Devices) @@ -1665,6 +1710,7 @@ public List GetServerAdmins() using var dbContext = _appDbFactory.GetContext(); return dbContext.Users + .AsNoTracking() .Where(x => x.IsServerAdmin) .Select(x => $"{x.UserName}") .ToList(); @@ -1698,7 +1744,9 @@ public async Task> GetUserById(string userId) } using var dbContext = _appDbFactory.GetContext(); - var user = await dbContext.Users.FirstOrDefaultAsync(x => x.Id == userId); + var user = await dbContext.Users + .AsNoTracking() + .FirstOrDefaultAsync(x => x.Id == userId); if (user is null) { @@ -1718,7 +1766,10 @@ public async Task> GetUserByName( using var dbContext = _appDbFactory.GetContext(); - var query = dbContext.Users.AsQueryable(); + var query = dbContext.Users + .AsNoTracking() + .AsQueryable(); + includesBuilder?.Invoke(query); var user = await query.FirstOrDefaultAsync(x => @@ -1736,7 +1787,8 @@ public async Task> GetUserOptions(string userName) using var dbContext = _appDbFactory.GetContext(); var user = await dbContext.Users - .FirstOrDefaultAsync(x => x.UserName == userName); + .AsNoTracking() + .FirstOrDefaultAsync(x => x.UserName == userName); if (user is null) { @@ -1758,24 +1810,27 @@ public async Task JoinViaInvitation(string userName, string inviteId) using var dbContext = _appDbFactory.GetContext(); - var invite = await dbContext.InviteLinks.FirstOrDefaultAsync(x => - x.InvitedUser!.ToLower() == userName.ToLower() && - x.ID == inviteId); + var invite = await dbContext.InviteLinks + .FirstOrDefaultAsync(x => + x.InvitedUser!.ToLower() == userName.ToLower() && + x.ID == inviteId); if (invite is null) { return Result.Fail("Invite not found."); } - var user = await dbContext.Users.FirstOrDefaultAsync(x => x.UserName == userName); + var user = await dbContext.Users + .FirstOrDefaultAsync(x => x.UserName == userName); + if (user is null) { return Result.Fail("User not found."); } var organization = await dbContext.Organizations - .Include(x => x.RemotelyUsers) - .FirstOrDefaultAsync(x => x.ID == invite.OrganizationID); + .Include(x => x.RemotelyUsers) + .FirstOrDefaultAsync(x => x.ID == invite.OrganizationID); if (organization is null) { From a3a1ad54256023149f31e78e29efdaa5a9079fd4 Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Wed, 26 Jul 2023 14:14:02 -0700 Subject: [PATCH 13/29] Fix change tracking issue. --- Server/Services/DataService.cs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/Server/Services/DataService.cs b/Server/Services/DataService.cs index 758d3088e..b11d8dda8 100644 --- a/Server/Services/DataService.cs +++ b/Server/Services/DataService.cs @@ -427,16 +427,21 @@ public async Task AddOrUpdateSavedScript(SavedScript script, string user { using var dbContext = _appDbFactory.GetContext(); - var user = await dbContext.Users.FindAsync(userId); - if (user is null) + dbContext.SavedScripts.Update(script); + + if (script.Creator is null) { - return Result.Fail("User not found."); + var user = await dbContext.Users.FindAsync(userId); + if (user is null) + { + return Result.Fail("User not found."); + } + + script.CreatorId = user.Id; + script.Creator = user; + script.OrganizationID = user.OrganizationID; } - dbContext.SavedScripts.Update(script); - script.CreatorId = user.Id; - script.Creator = user; - script.OrganizationID = user.OrganizationID; await dbContext.SaveChangesAsync(); return Result.Ok(); } From 2ac886a03e1e3ff397c2944860e40a1c4c20a518 Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Wed, 26 Jul 2023 14:35:58 -0700 Subject: [PATCH 14/29] Create default debug org. --- Agent/Services/AgentHubConnection.cs | 5 ++--- Agent/Services/ConfigService.cs | 3 ++- .../Identity/Pages/Account/Register.cshtml.cs | 17 ++++++++++++++++- Shared/AppConstants.cs | 1 + 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/Agent/Services/AgentHubConnection.cs b/Agent/Services/AgentHubConnection.cs index 40f102696..e68a2683a 100644 --- a/Agent/Services/AgentHubConnection.cs +++ b/Agent/Services/AgentHubConnection.cs @@ -81,13 +81,12 @@ public AgentHubConnection( public async Task Connect() { using var throttle = new SemaphoreSlim(1, 1); - var count = 1; - while (true) + for (var i = 1; true; i++) { try { - var waitSeconds = Math.Min(60, Math.Pow(count, 2)); + var waitSeconds = Math.Min(60, Math.Pow(i, 2)); // This will allow the first attempt to go through immediately, but // subsequent attempts will have an exponential delay. _ = await throttle.WaitAsync(TimeSpan.FromSeconds(waitSeconds)); diff --git a/Agent/Services/ConfigService.cs b/Agent/Services/ConfigService.cs index 6eaea12c6..fafb8f99e 100644 --- a/Agent/Services/ConfigService.cs +++ b/Agent/Services/ConfigService.cs @@ -1,5 +1,6 @@ using Immense.RemoteControl.Shared; using Microsoft.Extensions.Logging; +using Remotely.Shared; using Remotely.Shared.Models; using Remotely.Shared.Utilities; using System; @@ -81,7 +82,7 @@ public ConnectionInfo GetConnectionInfo() { DeviceID = _debugGuid, Host = "http://localhost:5000", - OrganizationID = orgID + OrganizationID = AppConstants.DebugOrgId }; } diff --git a/Server/Areas/Identity/Pages/Account/Register.cshtml.cs b/Server/Areas/Identity/Pages/Account/Register.cshtml.cs index e2d1b21f5..21d20bf80 100644 --- a/Server/Areas/Identity/Pages/Account/Register.cshtml.cs +++ b/Server/Areas/Identity/Pages/Account/Register.cshtml.cs @@ -7,13 +7,16 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.UI.Services; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Remotely.Server.Services; +using Remotely.Shared; using Remotely.Shared.Models; namespace Remotely.Server.Areas.Identity.Pages.Account; @@ -26,6 +29,7 @@ public class RegisterModel : PageModel private readonly ILogger _logger; private readonly IEmailSenderEx _emailSender; private readonly IDataService _dataService; + private readonly IWebHostEnvironment _hostEnvironment; private readonly IApplicationConfig _appConfig; public RegisterModel( @@ -34,6 +38,7 @@ public RegisterModel( ILogger logger, IEmailSenderEx emailSender, IDataService dataService, + IWebHostEnvironment hostEnvironment, IApplicationConfig appConfig) { _userManager = userManager; @@ -41,6 +46,7 @@ public RegisterModel( _logger = logger; _emailSender = emailSender; _dataService = dataService; + _hostEnvironment = hostEnvironment; _appConfig = appConfig; } @@ -95,12 +101,21 @@ public async Task OnPostAsync(string? returnUrl = null) UserName = Input.Email, Email = Input.Email, IsServerAdmin = organizationCount == 0, - Organization = new Organization() { OrganizationName = "Test Org" }, + Organization = new Organization() + { + OrganizationName = string.Empty, + IsDefaultOrganization = organizationCount == 0 + }, UserOptions = new RemotelyUserOptions(), IsAdministrator = true, LockoutEnabled = true }; + if (organizationCount == 0 && _hostEnvironment.IsDevelopment()) + { + user.Organization.ID = AppConstants.DebugOrgId; + } + var result = await _userManager.CreateAsync(user, Input.Password); if (result.Succeeded) { diff --git a/Shared/AppConstants.cs b/Shared/AppConstants.cs index d2ab8a695..98a263205 100644 --- a/Shared/AppConstants.cs +++ b/Shared/AppConstants.cs @@ -10,6 +10,7 @@ public class AppConstants { public const string DefaultProductName = "Remotely"; public const string DefaultPublisherName = "Immense Networks"; + public const string DebugOrgId = "e8f4ad87-4a4b-4da1-bcb2-1788eaeb80e8"; public const int EmbeddedDataBlockLength = 256; public const long MaxUploadFileSize = 100_000_000; public const double ScriptRunExpirationMinutes = 30; From c8d1e9c26b8e29aacbae7de6fac1697832403254 Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Wed, 26 Jul 2023 14:36:05 -0700 Subject: [PATCH 15/29] Make Device nullable. --- Server/Hubs/AgentHub.cs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/Server/Hubs/AgentHub.cs b/Server/Hubs/AgentHub.cs index e24ec7df2..f54c185c4 100644 --- a/Server/Hubs/AgentHub.cs +++ b/Server/Hubs/AgentHub.cs @@ -46,7 +46,7 @@ public AgentHub(IDataService dataService, // TODO: Replace with new invoke capability in .NET 7 in ScriptingController. public static IMemoryCache ApiScriptResults { get; } = new MemoryCache(new MemoryCacheOptions()); - private Device Device + private Device? Device { get { @@ -54,10 +54,8 @@ private Device Device { return device; } - else - { - throw new InvalidOperationException("Device not set."); - } + _logger.LogWarning("Device has not been set in the context items."); + return null; } set { @@ -67,6 +65,11 @@ private Device Device public Task Chat(string message, bool disconnected, string browserConnectionId) { + if (Device is null) + { + return Task.CompletedTask; + } + if (_circuitManager.TryGetConnection(browserConnectionId, out var connection)) { return connection.InvokeCircuitEvent(CircuitEventName.ChatReceived, Device.ID, $"{Device.DeviceName}", message, disconnected); @@ -80,6 +83,11 @@ public Task Chat(string message, bool disconnected, string browserConnectionId) public async Task CheckForPendingScriptRuns() { + if (Device is null) + { + return; + } + var authToken = _expiringTokenService.GetToken(Time.Now.AddMinutes(AppConstants.ScriptRunExpirationMinutes)); var scriptRuns = await _dataService.GetPendingScriptRuns(Device.ID); foreach (var run in scriptRuns) @@ -221,7 +229,7 @@ public string GetServerUrl() public string GetServerVerificationToken() { - return $"{Device.ServerVerificationToken}"; + return $"{Device?.ServerVerificationToken}"; } public override Task OnDisconnectedAsync(Exception? exception) @@ -288,6 +296,10 @@ public Task SendLogs(string logChunk, string requesterConnectionId) } public void SetServerVerificationToken(string verificationToken) { + if (Device is null) + { + return; + } Device.ServerVerificationToken = verificationToken; _dataService.SetServerVerificationToken(Device.ID, verificationToken); } From 6e1c2318a46c5f2e9fe5847a1c78974f252d8738 Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Wed, 26 Jul 2023 15:22:43 -0700 Subject: [PATCH 16/29] Create new convention for transferring DTOs to entities. --- Agent/Services/ExternalScriptingShell.cs | 13 +++-- Agent/Services/PsCoreShell.cs | 9 +-- Agent/Services/ScriptExecutor.cs | 5 +- Server/API/ScriptResultsController.cs | 29 +++++++--- Server/Components/AuthComponentBase.cs | 2 + Server/Components/AuthorizedIndex.razor | 6 +- Server/Services/DataService.cs | 57 +++++++++---------- Shared/DtoEntityBases/ScriptResultBase.cs | 37 ++++++++++++ Shared/Dtos/ScriptResultDto.cs | 13 +++++ .../{Models => Dtos}/ScriptResultResponse.cs | 2 +- Shared/Models/ScriptResult.cs | 36 ++---------- 11 files changed, 126 insertions(+), 83 deletions(-) create mode 100644 Shared/DtoEntityBases/ScriptResultBase.cs create mode 100644 Shared/Dtos/ScriptResultDto.cs rename Shared/{Models => Dtos}/ScriptResultResponse.cs (85%) diff --git a/Agent/Services/ExternalScriptingShell.cs b/Agent/Services/ExternalScriptingShell.cs index 37f7a58fa..abad72a47 100644 --- a/Agent/Services/ExternalScriptingShell.cs +++ b/Agent/Services/ExternalScriptingShell.cs @@ -1,6 +1,7 @@ using Immense.RemoteControl.Shared.Extensions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Remotely.Shared.Dtos; using Remotely.Shared.Enums; using Remotely.Shared.Models; using System; @@ -17,7 +18,7 @@ public interface IExternalScriptingShell { Process? ShellProcess { get; } Task Init(ScriptingShell shell, string shellProcessName, string lineEnding, string connectionId); - Task WriteInput(string input, TimeSpan timeout); + Task WriteInput(string input, TimeSpan timeout); } public class ExternalScriptingShell : IExternalScriptingShell @@ -129,7 +130,7 @@ public async Task Init(ScriptingShell shell, string shellProcessName, string lin } } - public async Task WriteInput(string input, TimeSpan timeout) + public async Task WriteInput(string input, TimeSpan timeout) { await _writeLock.WaitAsync(); var sw = Stopwatch.StartNew(); @@ -186,9 +187,9 @@ public async Task WriteInput(string input, TimeSpan timeout) return GeneratePartialResult(input, sw.Elapsed); } - private ScriptResult GenerateCompletedResult(string input, TimeSpan runtime) + private ScriptResultDto GenerateCompletedResult(string input, TimeSpan runtime) { - return new ScriptResult() + return new ScriptResultDto() { Shell = _shell, RunTime = runtime, @@ -202,9 +203,9 @@ private ScriptResult GenerateCompletedResult(string input, TimeSpan runtime) }; } - private ScriptResult GeneratePartialResult(string input, TimeSpan runtime) + private ScriptResultDto GeneratePartialResult(string input, TimeSpan runtime) { - var partialResult = new ScriptResult() + var partialResult = new ScriptResultDto() { Shell = _shell, RunTime = runtime, diff --git a/Agent/Services/PsCoreShell.cs b/Agent/Services/PsCoreShell.cs index d115e50f4..4e264f581 100644 --- a/Agent/Services/PsCoreShell.cs +++ b/Agent/Services/PsCoreShell.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Remotely.Shared.Dtos; using Remotely.Shared.Models; using System; using System.Collections.Concurrent; @@ -15,7 +16,7 @@ public interface IPsCoreShell string? SenderConnectionId { get; set; } CommandCompletion GetCompletions(string inputText, int currentIndex, bool? forward); - Task WriteInput(string input); + Task WriteInput(string input); } public class PsCoreShell : IPsCoreShell @@ -83,7 +84,7 @@ public CommandCompletion GetCompletions(string inputText, int currentIndex, bool return _lastCompletion; } - public async Task WriteInput(string input) + public async Task WriteInput(string input) { var deviceId = _configService.GetConnectionInfo().DeviceID; var sw = Stopwatch.StartNew(); @@ -121,7 +122,7 @@ public async Task WriteInput(string input) var errorAndWarningOut = errorOut.Concat(warningOut).ToArray(); - return new ScriptResult() + return new ScriptResultDto() { DeviceID = _configService.GetConnectionInfo().DeviceID, SenderConnectionID = SenderConnectionId, @@ -136,7 +137,7 @@ public async Task WriteInput(string input) catch (Exception ex) { _logger.LogError(ex, "Error while writing input to PSCore."); - return new ScriptResult() + return new ScriptResultDto() { DeviceID = deviceId, SenderConnectionID = SenderConnectionId, diff --git a/Agent/Services/ScriptExecutor.cs b/Agent/Services/ScriptExecutor.cs index 262e7cf47..67b200dc0 100644 --- a/Agent/Services/ScriptExecutor.cs +++ b/Agent/Services/ScriptExecutor.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Remotely.Shared; +using Remotely.Shared.Dtos; using Remotely.Shared.Enums; using Remotely.Shared.Models; using Remotely.Shared.Utilities; @@ -151,7 +152,7 @@ public async Task RunScript(Guid savedScriptId, } } - private async Task ExecuteScriptContent( + private async Task ExecuteScriptContent( ScriptingShell shell, string terminalSessionId, string command, @@ -196,7 +197,7 @@ private async Task ExecuteScriptContent( } throw new InvalidOperationException($"Unknown shell type: {shell}"); } - private async Task SendResultsToApi(ScriptResult result, string expiringToken) + private async Task SendResultsToApi(ScriptResultDto result, string expiringToken) { var targetURL = _configService.GetConnectionInfo().Host + $"/API/ScriptResults"; diff --git a/Server/API/ScriptResultsController.cs b/Server/API/ScriptResultsController.cs index 8936763fe..be417443a 100644 --- a/Server/API/ScriptResultsController.cs +++ b/Server/API/ScriptResultsController.cs @@ -1,8 +1,12 @@ -using Microsoft.AspNetCore.Mvc; +using Immense.RemoteControl.Shared.Extensions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Build.Framework; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using Remotely.Server.Auth; using Remotely.Server.Extensions; using Remotely.Server.Services; +using Remotely.Shared.Dtos; using Remotely.Shared.Models; using System; using System.Text; @@ -16,11 +20,16 @@ public class ScriptResultsController : ControllerBase { private readonly IDataService _dataService; private readonly IEmailSenderEx _emailSender; + private readonly ILogger _logger; - public ScriptResultsController(IDataService dataService, IEmailSenderEx emailSenderEx) + public ScriptResultsController( + IDataService dataService, + IEmailSenderEx emailSenderEx, + ILogger logger) { _dataService = dataService; _emailSender = emailSenderEx; + _logger = logger; } [HttpGet] @@ -54,9 +63,15 @@ public ActionResult DownloadResults(string scriptId) [HttpPost] [ServiceFilter(typeof(ExpiringTokenFilter))] - public async Task> Post([FromBody] ScriptResult result) + public async Task> Post([FromBody] ScriptResultDto result) { - _dataService.AddOrUpdateScriptResult(result); + var scriptResult = await _dataService.AddScriptResult(result); + + if (!scriptResult.IsSuccess) + { + _logger.LogResult(scriptResult); + return BadRequest(); + } var errorOut = result.ErrorOutput ?? Array.Empty(); @@ -72,7 +87,7 @@ public async Task> Post([FromBody] ScriptResu if (savedScript.GenerateAlertOnError) { await _dataService.AddAlert(result.DeviceID, - result.OrganizationID, + savedScript.OrganizationID, $"Alert triggered while running script {savedScript.Name}.", string.Join("\n", errorOut)); } @@ -108,12 +123,12 @@ await _emailSender.SendEmailAsync(savedScript.SendErrorEmailTo, if (result.ScriptRunId.HasValue) { - await _dataService.AddScriptResultToScriptRun(result.ID, result.ScriptRunId.Value); + await _dataService.AddScriptResultToScriptRun(scriptResult.Value.ID, result.ScriptRunId.Value); } return new ScriptResultResponse() { - Id = result.ID + Id = scriptResult.Value.ID }; } } diff --git a/Server/Components/AuthComponentBase.cs b/Server/Components/AuthComponentBase.cs index 5666ed963..4090e39ed 100644 --- a/Server/Components/AuthComponentBase.cs +++ b/Server/Components/AuthComponentBase.cs @@ -28,6 +28,8 @@ protected override async Task OnInitializedAsync() public bool IsAuthenticated { get; private set; } + public bool IsUserSet => _user is not null; + public RemotelyUser User { get => _user ?? throw new InvalidOperationException("User has not been resolved yet."); diff --git a/Server/Components/AuthorizedIndex.razor b/Server/Components/AuthorizedIndex.razor index f898e4e94..b8a8fa586 100644 --- a/Server/Components/AuthorizedIndex.razor +++ b/Server/Components/AuthorizedIndex.razor @@ -16,11 +16,11 @@ -@code { +@code { protected override void OnAfterRender(bool firstRender) { - if (AppConfig.Require2FA && !User.TwoFactorEnabled) + if (AppConfig.Require2FA && IsUserSet && !User.TwoFactorEnabled) { NavManager.NavigateTo("/TwoFactorRequired"); } @@ -31,7 +31,7 @@ { await base.OnInitializedAsync(); - if (IsAuthenticated == true && User is null) + if (IsAuthenticated == true && !IsUserSet) { await SignInManager.SignOutAsync(); NavManager.NavigateTo("/"); diff --git a/Server/Services/DataService.cs b/Server/Services/DataService.cs index b11d8dda8..309f592f1 100644 --- a/Server/Services/DataService.cs +++ b/Server/Services/DataService.cs @@ -36,10 +36,9 @@ public interface IDataService Task AddOrUpdateSavedScript(SavedScript script, string userId); - void AddOrUpdateScriptResult(ScriptResult scriptResult); - Task AddOrUpdateScriptSchedule(ScriptSchedule schedule); + Task> AddScriptResult(ScriptResultDto dto); Task AddScriptResultToScriptRun(string scriptResultId, int scriptRunId); Task AddScriptRun(ScriptRun scriptRun); @@ -228,8 +227,8 @@ Task UpdateBrandingInfo( public class DataService : IDataService { private readonly IApplicationConfig _appConfig; - private readonly IHostEnvironment _hostEnvironment; private readonly IAppDbFactory _appDbFactory; + private readonly IHostEnvironment _hostEnvironment; private readonly ILogger _logger; public DataService( @@ -446,33 +445,6 @@ public async Task AddOrUpdateSavedScript(SavedScript script, string user return Result.Ok(); } - public void AddOrUpdateScriptResult(ScriptResult result) - { - using var dbContext = _appDbFactory.GetContext(); - - var device = dbContext.Devices.Find(result.DeviceID); - - if (device is null) - { - return; - } - - result.OrganizationID = device.OrganizationID; - - var existingResult = dbContext.ScriptResults.Find(result.ID); - if (existingResult is not null) - { - var entry = dbContext.Entry(existingResult); - entry.CurrentValues.SetValues(result); - entry.State = EntityState.Modified; - } - else - { - dbContext.ScriptResults.Add(result); - } - dbContext.SaveChanges(); - } - public async Task AddOrUpdateScriptSchedule(ScriptSchedule schedule) { using var dbContext = _appDbFactory.GetContext(); @@ -516,6 +488,31 @@ public async Task AddOrUpdateScriptSchedule(ScriptSchedule schedule) await dbContext.SaveChangesAsync(); } + public async Task> AddScriptResult(ScriptResultDto dto) + { + using var dbContext = _appDbFactory.GetContext(); + + var device = dbContext.Devices.Find(dto.DeviceID); + + if (device is null) + { + return Result.Fail("Device not found."); + } + + var scriptResult = new ScriptResult + { + DeviceID = dto.DeviceID, + OrganizationID = device.OrganizationID + }; + + var entry = dbContext.Attach(scriptResult); + entry.CurrentValues.SetValues(dto); + entry.State = EntityState.Added; + await dbContext.ScriptResults.AddAsync(scriptResult); + await dbContext.SaveChangesAsync(); + return Result.Ok(scriptResult); + } + public async Task AddScriptResultToScriptRun(string scriptResultId, int scriptRunId) { using var dbContext = _appDbFactory.GetContext(); diff --git a/Shared/DtoEntityBases/ScriptResultBase.cs b/Shared/DtoEntityBases/ScriptResultBase.cs new file mode 100644 index 000000000..a277c4dad --- /dev/null +++ b/Shared/DtoEntityBases/ScriptResultBase.cs @@ -0,0 +1,37 @@ +using Remotely.Shared.Enums; +using Remotely.Shared.Models; +using Remotely.Shared.Utilities; +using System; +using System.Text.Json.Serialization; + +namespace Remotely.Shared.DtoEntityBases; + +public abstract class ScriptResultBase +{ + public virtual required string DeviceID { get; set; } + + public virtual string[]? ErrorOutput { get; set; } + + public virtual bool HadErrors { get; set; } + + public virtual ScriptInputType InputType { get; set; } + + [JsonConverter(typeof(TimeSpanJsonConverter))] + public virtual TimeSpan RunTime { get; set; } + + public virtual string ScriptInput { get; set; } = string.Empty; + + public virtual int? ScheduleId { get; set; } + public virtual Guid? SavedScriptId { get; set; } + + public virtual int? ScriptRunId { get; set; } + + + public virtual string? SenderConnectionID { get; set; } + public virtual string? SenderUserName { get; set; } = null!; + public virtual ScriptingShell Shell { get; set; } + public virtual string[]? StandardOutput { get; set; } + + public virtual DateTimeOffset TimeStamp { get; set; } = DateTimeOffset.Now; + +} diff --git a/Shared/Dtos/ScriptResultDto.cs b/Shared/Dtos/ScriptResultDto.cs new file mode 100644 index 000000000..75162b52a --- /dev/null +++ b/Shared/Dtos/ScriptResultDto.cs @@ -0,0 +1,13 @@ +using Remotely.Shared.DtoEntityBases; +using Remotely.Shared.Enums; +using Remotely.Shared.Models; +using Remotely.Shared.Utilities; +using System; +using System.Text.Json.Serialization; + +namespace Remotely.Shared.Dtos; + +public class ScriptResultDto : ScriptResultBase +{ + +} diff --git a/Shared/Models/ScriptResultResponse.cs b/Shared/Dtos/ScriptResultResponse.cs similarity index 85% rename from Shared/Models/ScriptResultResponse.cs rename to Shared/Dtos/ScriptResultResponse.cs index ea6ceaddf..417db7014 100644 --- a/Shared/Models/ScriptResultResponse.cs +++ b/Shared/Dtos/ScriptResultResponse.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace Remotely.Shared.Models; +namespace Remotely.Shared.Dtos; public class ScriptResultResponse { public required string Id { get; init; } diff --git a/Shared/Models/ScriptResult.cs b/Shared/Models/ScriptResult.cs index 440c2f1e8..88ec2f1fe 100644 --- a/Shared/Models/ScriptResult.cs +++ b/Shared/Models/ScriptResult.cs @@ -1,4 +1,5 @@ -using Remotely.Shared.Enums; +using Remotely.Shared.DtoEntityBases; +using Remotely.Shared.Enums; using Remotely.Shared.Utilities; using System; using System.Collections.Generic; @@ -9,48 +10,23 @@ namespace Remotely.Shared.Models; -public class ScriptResult +public class ScriptResult : ScriptResultBase { [JsonIgnore] public Device? Device { get; set; } - public string DeviceID { get; set; } = null!; - - public string[]? ErrorOutput { get; set; } - - public bool HadErrors { get; set; } - [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public string ID { get; set; } = null!; - - - public ScriptInputType InputType { get; set; } + public string ID { get; set; } = string.Empty; [JsonIgnore] [IgnoreDataMember] public Organization? Organization { get; set; } - public string OrganizationID { get; set; } = null!; - - [JsonConverter(typeof(TimeSpanJsonConverter))] - public TimeSpan RunTime { get; set; } + public string OrganizationID { get; set; } = string.Empty; [JsonIgnore] public ScriptSchedule? Schedule { get; set; } - public required string ScriptInput { get; set; } - - public int? ScheduleId { get; set; } - public Guid? SavedScriptId { get; set; } - + [JsonIgnore] public ScriptRun? ScriptRun { get; set; } - public int? ScriptRunId { get; set; } - - - public string? SenderConnectionID { get; set; } - public string? SenderUserName { get; set; } = null!; - public ScriptingShell Shell { get; set; } - public string[]? StandardOutput { get; set; } - - public DateTimeOffset TimeStamp { get; set; } = DateTimeOffset.Now; } From 9bf53192da36d7b2bb0d30322915ca120dab7c7e Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Wed, 26 Jul 2023 15:30:25 -0700 Subject: [PATCH 17/29] Reconfigure script relationships. --- Server/Data/AppDb.cs | 8 ++++++++ Shared/Models/SavedScript.cs | 6 ++++++ Shared/Models/ScriptResult.cs | 3 +++ Shared/Models/ScriptRun.cs | 6 ++++++ Tests/Server.Tests/DataServiceTests.cs | 5 ++--- 5 files changed, 25 insertions(+), 3 deletions(-) diff --git a/Server/Data/AppDb.cs b/Server/Data/AppDb.cs index a308ce8f9..7ec77eea5 100644 --- a/Server/Data/AppDb.cs +++ b/Server/Data/AppDb.cs @@ -161,6 +161,10 @@ protected override void OnModelCreating(ModelBuilder builder) .HasMany(x => x.Results) .WithOne(x => x.ScriptRun) .IsRequired(false); + builder.Entity() + .HasOne(x => x.SavedScript) + .WithMany(x => x.ScriptRuns) + .IsRequired(false); builder.Entity() .Property(x => x.ErrorOutput) @@ -181,6 +185,10 @@ protected override void OnModelCreating(ModelBuilder builder) .HasOne(x => x.ScriptRun) .WithMany(x => x.Results) .IsRequired(false); + builder.Entity() + .HasOne(x => x.SavedScript) + .WithMany(x => x.ScriptResults) + .IsRequired(false); builder.Entity() .HasOne(x => x.User) diff --git a/Shared/Models/SavedScript.cs b/Shared/Models/SavedScript.cs index 9e31da0d6..2d91fedb0 100644 --- a/Shared/Models/SavedScript.cs +++ b/Shared/Models/SavedScript.cs @@ -47,4 +47,10 @@ public class SavedScript public string? SendErrorEmailTo { get; set; } public ScriptingShell Shell { get; set; } + + [JsonIgnore] + public List? ScriptRuns { get; set; } + + [JsonIgnore] + public List? ScriptResults { get; set; } } diff --git a/Shared/Models/ScriptResult.cs b/Shared/Models/ScriptResult.cs index 88ec2f1fe..54db01c0f 100644 --- a/Shared/Models/ScriptResult.cs +++ b/Shared/Models/ScriptResult.cs @@ -24,6 +24,9 @@ public class ScriptResult : ScriptResultBase public Organization? Organization { get; set; } public string OrganizationID { get; set; } = string.Empty; + [JsonIgnore] + public SavedScript? SavedScript { get; set; } + [JsonIgnore] public ScriptSchedule? Schedule { get; set; } diff --git a/Shared/Models/ScriptRun.cs b/Shared/Models/ScriptRun.cs index 929dcfc6b..90c610241 100644 --- a/Shared/Models/ScriptRun.cs +++ b/Shared/Models/ScriptRun.cs @@ -32,6 +32,12 @@ public class ScriptRun public DateTimeOffset RunAt { get; set; } public bool RunOnNextConnect { get; set; } + + [JsonIgnore] + public SavedScript? SavedScript { get; set; } public Guid? SavedScriptId { get; set; } + + [JsonIgnore] + public ScriptSchedule? Schedule { get; set; } public int? ScheduleId { get; set; } } diff --git a/Tests/Server.Tests/DataServiceTests.cs b/Tests/Server.Tests/DataServiceTests.cs index afec5c7ce..003f7676e 100644 --- a/Tests/Server.Tests/DataServiceTests.cs +++ b/Tests/Server.Tests/DataServiceTests.cs @@ -162,18 +162,17 @@ public async Task GetPendingScriptRuns_GivenMultipleRunsQueued_ReturnsOnlyLatest Assert.AreEqual(1, pendingRuns.Count()); Assert.AreEqual(2, pendingRuns.First().Id); - var scriptResult = new ScriptResult() + var dto = new ScriptResultDto() { DeviceID = _testData.Org1Device1.ID, InputType = Shared.Enums.ScriptInputType.ScheduledScript, - OrganizationID = _testData.Org1Id, SavedScriptId = savedScript.Id, ScriptRunId = scriptRun.Id, Shell = Shared.Enums.ScriptingShell.PSCore, ScriptInput = "echo test" }; - _dataService.AddOrUpdateScriptResult(scriptResult); + var scriptResult = (await _dataService.AddScriptResult(dto)).Value!; await _dataService.AddScriptResultToScriptRun(scriptResult.ID, scriptRun.Id); From aa11983acfced9b1cd8792e2547e301b3f163242 Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Wed, 26 Jul 2023 15:56:24 -0700 Subject: [PATCH 18/29] Refactor script results and move entities. --- Agent/Services/ScriptExecutor.cs | 2 +- Desktop.Shared/Services/BrandingProvider.cs | 2 +- Server/API/BrandingController.cs | 2 +- Server/API/DevicesController.cs | 1 + Server/API/LoginController.cs | 2 +- Server/API/OrganizationManagementController.cs | 1 + Server/API/RemoteControlController.cs | 2 +- Server/API/SavedScriptsController.cs | 2 +- Server/API/ScriptingController.cs | 2 +- Server/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs | 2 +- .../Identity/Pages/Account/ConfirmEmailChange.cshtml.cs | 2 +- .../Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs | 2 +- Server/Areas/Identity/Pages/Account/Login.cshtml.cs | 2 +- Server/Areas/Identity/Pages/Account/Logout.cshtml | 1 + .../Pages/Account/Manage/EnableAuthenticator.cshtml.cs | 2 +- Server/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs | 2 +- .../Identity/Pages/Account/Manage/_ViewImports.cshtml | 3 ++- Server/Areas/Identity/Pages/Account/Register.cshtml.cs | 1 + Server/Areas/Identity/Pages/Shared/_LoginPartial.cshtml | 1 + Server/Auth/OrganizationAdminRequirementHandler.cs | 2 +- Server/Auth/ServerAdminRequirementHandler.cs | 2 +- Server/Auth/TwoFactorRequiredHandler.cs | 2 +- Server/Components/AuthComponentBase.cs | 2 +- Server/Components/Devices/DeviceCard.razor.cs | 1 + Server/Components/Devices/DevicesFrame.razor.cs | 1 + Server/Components/Devices/Terminal.razor.cs | 1 + Server/Components/Scripts/RunScript.razor.cs | 1 + Server/Components/Scripts/SavedScripts.razor.cs | 2 +- Server/Components/Scripts/ScriptSchedules.razor.cs | 1 + Server/Components/Scripts/ScriptTreeNode.cs | 2 +- Server/Data/AppDb.cs | 1 + Server/Hubs/AgentHub.cs | 3 ++- Server/Hubs/CircuitConnection.cs | 2 +- Server/Pages/ApiKeys.razor | 1 + Server/Pages/DeviceDetails.razor.cs | 2 +- Server/Pages/ManageOrganization.razor.cs | 1 + Server/Pages/ServerConfig.razor.cs | 2 +- Server/Pages/Shared/_Layout.cshtml | 1 + Server/Pages/Shared/_LoginPartial.cshtml | 2 +- Server/Pages/_Host.cshtml | 1 + Server/Program.cs | 2 +- Server/Services/AgentHubSessionCache.cs | 2 +- Server/Services/AuthService.cs | 2 +- Server/Services/DataService.cs | 1 + Server/Services/ScriptScheduleDispatcher.cs | 2 +- Server/_Imports.razor | 3 ++- Shared/{Models => Entities}/Alert.cs | 2 +- Shared/{Models => Entities}/ApiToken.cs | 2 +- Shared/{Models => Entities}/BrandingInfo.cs | 6 +++--- Shared/{Models => Entities}/Device.cs | 7 ++++--- Shared/{Models => Entities}/InviteLink.cs | 4 ++-- Shared/{Models => Entities}/Organization.cs | 3 ++- Shared/{Models => Entities}/RemotelyUser.cs | 3 ++- Shared/{Models => Entities}/SavedScript.cs | 2 +- Shared/{Models => Entities}/ScriptResult.cs | 6 +++--- Shared/{Models => Entities}/ScriptRun.cs | 2 +- Shared/{Models => Entities}/ScriptSchedule.cs | 3 ++- Shared/{Models => Entities}/SharedFile.cs | 2 +- Shared/Extensions/DeviceExtensions.cs | 2 +- Shared/Models/DeviceGroup.cs | 1 + Shared/Shared.csproj | 4 ++-- Tests/Server.Tests/DataServiceTests.cs | 1 + Tests/Server.Tests/IoCActivator.cs | 2 +- Tests/Server.Tests/ScriptScheduleDispatcherTests.cs | 1 + Tests/Server.Tests/TestData.cs | 1 + 65 files changed, 80 insertions(+), 53 deletions(-) rename Shared/{Models => Entities}/Alert.cs (95%) rename Shared/{Models => Entities}/ApiToken.cs (94%) rename Shared/{Models => Entities}/BrandingInfo.cs (78%) rename Shared/{Models => Entities}/Device.cs (95%) rename Shared/{Models => Entities}/InviteLink.cs (85%) rename Shared/{Models => Entities}/Organization.cs (95%) rename Shared/{Models => Entities}/RemotelyUser.cs (92%) rename Shared/{Models => Entities}/SavedScript.cs (97%) rename Shared/{Models => Entities}/ScriptResult.cs (84%) rename Shared/{Models => Entities}/ScriptRun.cs (96%) rename Shared/{Models => Entities}/ScriptSchedule.cs (95%) rename Shared/{Models => Entities}/SharedFile.cs (94%) diff --git a/Agent/Services/ScriptExecutor.cs b/Agent/Services/ScriptExecutor.cs index 67b200dc0..ee470e71f 100644 --- a/Agent/Services/ScriptExecutor.cs +++ b/Agent/Services/ScriptExecutor.cs @@ -4,8 +4,8 @@ using Microsoft.Extensions.Logging; using Remotely.Shared; using Remotely.Shared.Dtos; +using Remotely.Shared.Entities; using Remotely.Shared.Enums; -using Remotely.Shared.Models; using Remotely.Shared.Utilities; using System; using System.Net.Http; diff --git a/Desktop.Shared/Services/BrandingProvider.cs b/Desktop.Shared/Services/BrandingProvider.cs index e9fe69f56..485869405 100644 --- a/Desktop.Shared/Services/BrandingProvider.cs +++ b/Desktop.Shared/Services/BrandingProvider.cs @@ -4,8 +4,8 @@ using Immense.RemoteControl.Shared.Models; using Microsoft.Extensions.Logging; using Remotely.Shared; +using Remotely.Shared.Entities; using Remotely.Shared.Enums; -using Remotely.Shared.Models; using Remotely.Shared.Services; using Remotely.Shared.Utilities; using System; diff --git a/Server/API/BrandingController.cs b/Server/API/BrandingController.cs index d9c6c9614..696bf0359 100644 --- a/Server/API/BrandingController.cs +++ b/Server/API/BrandingController.cs @@ -4,7 +4,7 @@ using Microsoft.Build.Framework; using Microsoft.Extensions.Logging; using Remotely.Server.Services; -using Remotely.Shared.Models; +using Remotely.Shared.Entities; using System; using System.Collections.Generic; using System.Linq; diff --git a/Server/API/DevicesController.cs b/Server/API/DevicesController.cs index b70370807..40fe8c0b7 100644 --- a/Server/API/DevicesController.cs +++ b/Server/API/DevicesController.cs @@ -5,6 +5,7 @@ using Remotely.Server.Auth; using Remotely.Server.Extensions; using Remotely.Server.Services; +using Remotely.Shared.Entities; using Remotely.Shared.Models; using System; using System.Collections.Generic; diff --git a/Server/API/LoginController.cs b/Server/API/LoginController.cs index fa8f3cf23..3268bfc9a 100644 --- a/Server/API/LoginController.cs +++ b/Server/API/LoginController.cs @@ -8,7 +8,7 @@ using Remotely.Server.Hubs; using Remotely.Server.Models; using Remotely.Server.Services; -using Remotely.Shared.Models; +using Remotely.Shared.Entities; using System; using System.Linq; using System.Threading.Tasks; diff --git a/Server/API/OrganizationManagementController.cs b/Server/API/OrganizationManagementController.cs index 323f1f601..c8d582292 100644 --- a/Server/API/OrganizationManagementController.cs +++ b/Server/API/OrganizationManagementController.cs @@ -11,6 +11,7 @@ using Remotely.Server.Auth; using Remotely.Server.Extensions; using Remotely.Server.Services; +using Remotely.Shared.Entities; using Remotely.Shared.Models; using Remotely.Shared.ViewModels; using System; diff --git a/Server/API/RemoteControlController.cs b/Server/API/RemoteControlController.cs index aba579bdb..a6ab6f992 100644 --- a/Server/API/RemoteControlController.cs +++ b/Server/API/RemoteControlController.cs @@ -4,7 +4,6 @@ using Remotely.Server.Hubs; using Remotely.Server.Models; using Remotely.Server.Services; -using Remotely.Shared.Models; using System; using System.Linq; using System.Threading.Tasks; @@ -14,6 +13,7 @@ using Immense.RemoteControl.Shared.Helpers; using Microsoft.Extensions.Logging; using Remotely.Server.Extensions; +using Remotely.Shared.Entities; // For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 diff --git a/Server/API/SavedScriptsController.cs b/Server/API/SavedScriptsController.cs index 86c1aebcc..8aed4a79c 100644 --- a/Server/API/SavedScriptsController.cs +++ b/Server/API/SavedScriptsController.cs @@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Mvc; using Remotely.Server.Auth; using Remotely.Server.Services; -using Remotely.Shared.Models; +using Remotely.Shared.Entities; using System; using System.Collections.Generic; using System.Linq; diff --git a/Server/API/ScriptingController.cs b/Server/API/ScriptingController.cs index d7d607f9e..f9cf6cb9c 100644 --- a/Server/API/ScriptingController.cs +++ b/Server/API/ScriptingController.cs @@ -4,7 +4,6 @@ using Remotely.Server.Hubs; using Remotely.Server.Services; using Remotely.Shared.Utilities; -using Remotely.Shared.Models; using System; using System.IO; using System.Threading.Tasks; @@ -13,6 +12,7 @@ using Immense.RemoteControl.Shared.Helpers; using Remotely.Shared; using Remotely.Server.Extensions; +using Remotely.Shared.Entities; namespace Remotely.Server.API; diff --git a/Server/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs b/Server/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs index 46058053f..e2daf0061 100644 --- a/Server/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs +++ b/Server/Areas/Identity/Pages/Account/ConfirmEmail.cshtml.cs @@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.WebUtilities; -using Remotely.Shared.Models; +using Remotely.Shared.Entities; namespace Remotely.Server.Areas.Identity.Pages.Account; diff --git a/Server/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml.cs b/Server/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml.cs index 983f20391..57da742c4 100644 --- a/Server/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml.cs +++ b/Server/Areas/Identity/Pages/Account/ConfirmEmailChange.cshtml.cs @@ -8,7 +8,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.WebUtilities; -using Remotely.Shared.Models; +using Remotely.Shared.Entities; namespace Remotely.Server.Areas.Identity.Pages.Account; diff --git a/Server/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs b/Server/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs index a4fb87fc4..69a1be3b2 100644 --- a/Server/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs +++ b/Server/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs @@ -10,9 +10,9 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.WebUtilities; -using Remotely.Shared.Models; using Remotely.Server.Services; using Microsoft.Extensions.Logging; +using Remotely.Shared.Entities; namespace Remotely.Server.Areas.Identity.Pages.Account; diff --git a/Server/Areas/Identity/Pages/Account/Login.cshtml.cs b/Server/Areas/Identity/Pages/Account/Login.cshtml.cs index d433aa522..de0d56020 100644 --- a/Server/Areas/Identity/Pages/Account/Login.cshtml.cs +++ b/Server/Areas/Identity/Pages/Account/Login.cshtml.cs @@ -11,10 +11,10 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.Extensions.Logging; -using Remotely.Shared.Models; using Remotely.Server.Services; using Microsoft.AspNetCore.WebUtilities; using System.Text; +using Remotely.Shared.Entities; namespace Remotely.Server.Areas.Identity.Pages.Account; diff --git a/Server/Areas/Identity/Pages/Account/Logout.cshtml b/Server/Areas/Identity/Pages/Account/Logout.cshtml index f320b0194..99b4830cc 100644 --- a/Server/Areas/Identity/Pages/Account/Logout.cshtml +++ b/Server/Areas/Identity/Pages/Account/Logout.cshtml @@ -4,6 +4,7 @@ @using Microsoft.AspNetCore.SignalR @using Remotely.Server.Hubs @using Immense.RemoteControl.Server.Hubs +@using Remotely.Shared.Entities @attribute [IgnoreAntiforgeryToken] @inject SignInManager SignInManager diff --git a/Server/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs b/Server/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs index 7a64628e2..f218e4e89 100644 --- a/Server/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs +++ b/Server/Areas/Identity/Pages/Account/Manage/EnableAuthenticator.cshtml.cs @@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.Extensions.Logging; -using Remotely.Shared.Models; +using Remotely.Shared.Entities; namespace Remotely.Server.Areas.Identity.Pages.Account.Manage; diff --git a/Server/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs b/Server/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs index 9ee3bd79b..8b2438177 100644 --- a/Server/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs +++ b/Server/Areas/Identity/Pages/Account/Manage/Index.cshtml.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; -using Remotely.Shared.Models; +using Remotely.Shared.Entities; namespace Remotely.Server.Areas.Identity.Pages.Account.Manage; diff --git a/Server/Areas/Identity/Pages/Account/Manage/_ViewImports.cshtml b/Server/Areas/Identity/Pages/Account/Manage/_ViewImports.cshtml index 6a917e1b3..de11ea2bd 100644 --- a/Server/Areas/Identity/Pages/Account/Manage/_ViewImports.cshtml +++ b/Server/Areas/Identity/Pages/Account/Manage/_ViewImports.cshtml @@ -1,2 +1,3 @@ @using Remotely.Server.Areas.Identity.Pages.Account.Manage -@using Remotely.Shared.Models \ No newline at end of file +@using Remotely.Shared.Models +@using Remotely.Shared.Entities \ No newline at end of file diff --git a/Server/Areas/Identity/Pages/Account/Register.cshtml.cs b/Server/Areas/Identity/Pages/Account/Register.cshtml.cs index 21d20bf80..53495e95f 100644 --- a/Server/Areas/Identity/Pages/Account/Register.cshtml.cs +++ b/Server/Areas/Identity/Pages/Account/Register.cshtml.cs @@ -17,6 +17,7 @@ using Microsoft.Extensions.Logging; using Remotely.Server.Services; using Remotely.Shared; +using Remotely.Shared.Entities; using Remotely.Shared.Models; namespace Remotely.Server.Areas.Identity.Pages.Account; diff --git a/Server/Areas/Identity/Pages/Shared/_LoginPartial.cshtml b/Server/Areas/Identity/Pages/Shared/_LoginPartial.cshtml index 28975f28b..7285c71b7 100644 --- a/Server/Areas/Identity/Pages/Shared/_LoginPartial.cshtml +++ b/Server/Areas/Identity/Pages/Shared/_LoginPartial.cshtml @@ -1,4 +1,5 @@ @using Microsoft.AspNetCore.Identity +@using Remotely.Shared.Entities @inject SignInManager SignInManager @inject UserManager UserManager @inject Remotely.Server.Services.IApplicationConfig AppConfig diff --git a/Server/Auth/OrganizationAdminRequirementHandler.cs b/Server/Auth/OrganizationAdminRequirementHandler.cs index 540e6d911..f0697b9f8 100644 --- a/Server/Auth/OrganizationAdminRequirementHandler.cs +++ b/Server/Auth/OrganizationAdminRequirementHandler.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; -using Remotely.Shared.Models; +using Remotely.Shared.Entities; using System.Threading.Tasks; namespace Remotely.Server.Auth; diff --git a/Server/Auth/ServerAdminRequirementHandler.cs b/Server/Auth/ServerAdminRequirementHandler.cs index e7a9f2034..a5c060ccf 100644 --- a/Server/Auth/ServerAdminRequirementHandler.cs +++ b/Server/Auth/ServerAdminRequirementHandler.cs @@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; -using Remotely.Shared.Models; +using Remotely.Shared.Entities; using System.Threading.Tasks; namespace Remotely.Server.Auth; diff --git a/Server/Auth/TwoFactorRequiredHandler.cs b/Server/Auth/TwoFactorRequiredHandler.cs index beb431f9d..ebeccff92 100644 --- a/Server/Auth/TwoFactorRequiredHandler.cs +++ b/Server/Auth/TwoFactorRequiredHandler.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Remotely.Server.Services; -using Remotely.Shared.Models; +using Remotely.Shared.Entities; using System; using System.Collections.Generic; using System.Linq; diff --git a/Server/Components/AuthComponentBase.cs b/Server/Components/AuthComponentBase.cs index 4090e39ed..371392a6c 100644 --- a/Server/Components/AuthComponentBase.cs +++ b/Server/Components/AuthComponentBase.cs @@ -2,7 +2,7 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Identity; using Remotely.Server.Services; -using Remotely.Shared.Models; +using Remotely.Shared.Entities; using System; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; diff --git a/Server/Components/Devices/DeviceCard.razor.cs b/Server/Components/Devices/DeviceCard.razor.cs index 7a037597b..c9ef0d3c4 100644 --- a/Server/Components/Devices/DeviceCard.razor.cs +++ b/Server/Components/Devices/DeviceCard.razor.cs @@ -9,6 +9,7 @@ using Remotely.Server.Hubs; using Remotely.Server.Models; using Remotely.Server.Services; +using Remotely.Shared.Entities; using Remotely.Shared.Enums; using Remotely.Shared.Models; using Remotely.Shared.Utilities; diff --git a/Server/Components/Devices/DevicesFrame.razor.cs b/Server/Components/Devices/DevicesFrame.razor.cs index 3d2cd22fb..292a90ad5 100644 --- a/Server/Components/Devices/DevicesFrame.razor.cs +++ b/Server/Components/Devices/DevicesFrame.razor.cs @@ -7,6 +7,7 @@ using Remotely.Server.Models; using Remotely.Server.Services; using Remotely.Shared.Attributes; +using Remotely.Shared.Entities; using Remotely.Shared.Models; using Remotely.Shared.Utilities; using System; diff --git a/Server/Components/Devices/Terminal.razor.cs b/Server/Components/Devices/Terminal.razor.cs index 8db4170b4..8ba370e56 100644 --- a/Server/Components/Devices/Terminal.razor.cs +++ b/Server/Components/Devices/Terminal.razor.cs @@ -6,6 +6,7 @@ using Remotely.Server.Components.ModalContents; using Remotely.Server.Hubs; using Remotely.Server.Services; +using Remotely.Shared.Entities; using Remotely.Shared.Enums; using Remotely.Shared.Models; using Remotely.Shared.Utilities; diff --git a/Server/Components/Scripts/RunScript.razor.cs b/Server/Components/Scripts/RunScript.razor.cs index 755926891..9dbfd2629 100644 --- a/Server/Components/Scripts/RunScript.razor.cs +++ b/Server/Components/Scripts/RunScript.razor.cs @@ -8,6 +8,7 @@ using Remotely.Server.Migrations.SqlServer; using Remotely.Server.Pages; using Remotely.Server.Services; +using Remotely.Shared.Entities; using Remotely.Shared.Enums; using Remotely.Shared.Models; using Remotely.Shared.Utilities; diff --git a/Server/Components/Scripts/SavedScripts.razor.cs b/Server/Components/Scripts/SavedScripts.razor.cs index 1cfc96755..9bf2609d2 100644 --- a/Server/Components/Scripts/SavedScripts.razor.cs +++ b/Server/Components/Scripts/SavedScripts.razor.cs @@ -4,7 +4,7 @@ using Microsoft.CodeAnalysis.Scripting; using Remotely.Server.Pages; using Remotely.Server.Services; -using Remotely.Shared.Models; +using Remotely.Shared.Entities; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; diff --git a/Server/Components/Scripts/ScriptSchedules.razor.cs b/Server/Components/Scripts/ScriptSchedules.razor.cs index 12dfc9e63..dacdb86e6 100644 --- a/Server/Components/Scripts/ScriptSchedules.razor.cs +++ b/Server/Components/Scripts/ScriptSchedules.razor.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Components.Forms; using Remotely.Server.Pages; using Remotely.Server.Services; +using Remotely.Shared.Entities; using Remotely.Shared.Models; using Remotely.Shared.Utilities; using System; diff --git a/Server/Components/Scripts/ScriptTreeNode.cs b/Server/Components/Scripts/ScriptTreeNode.cs index e3d9e76a5..86384907a 100644 --- a/Server/Components/Scripts/ScriptTreeNode.cs +++ b/Server/Components/Scripts/ScriptTreeNode.cs @@ -1,5 +1,5 @@ using Remotely.Server.Components.TreeView; -using Remotely.Shared.Models; +using Remotely.Shared.Entities; using System; using System.Collections.Generic; using System.Linq; diff --git a/Server/Data/AppDb.cs b/Server/Data/AppDb.cs index 7ec77eea5..b9112e81b 100644 --- a/Server/Data/AppDb.cs +++ b/Server/Data/AppDb.cs @@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; +using Remotely.Shared.Entities; using Remotely.Shared.Models; using Remotely.Shared.Utilities; using System; diff --git a/Server/Hubs/AgentHub.cs b/Server/Hubs/AgentHub.cs index f54c185c4..a4c42404d 100644 --- a/Server/Hubs/AgentHub.cs +++ b/Server/Hubs/AgentHub.cs @@ -6,6 +6,7 @@ using Remotely.Server.Services; using Remotely.Shared; using Remotely.Shared.Dtos; +using Remotely.Shared.Entities; using Remotely.Shared.Enums; using Remotely.Shared.Models; using Remotely.Shared.Utilities; @@ -278,7 +279,7 @@ public async Task ScriptResult(string scriptResultId) _ = await _circuitManager.InvokeOnConnection($"{result.Value.SenderConnectionID}", CircuitEventName.ScriptResult, - result); + result.Value); } public void ScriptResultViaApi(string commandID, string requestID) diff --git a/Server/Hubs/CircuitConnection.cs b/Server/Hubs/CircuitConnection.cs index e50ee796a..1bc1bc609 100644 --- a/Server/Hubs/CircuitConnection.cs +++ b/Server/Hubs/CircuitConnection.cs @@ -12,8 +12,8 @@ using Remotely.Server.Models; using Remotely.Server.Services; using Remotely.Shared; +using Remotely.Shared.Entities; using Remotely.Shared.Enums; -using Remotely.Shared.Models; using Remotely.Shared.Utilities; using System; using System.Collections.Concurrent; diff --git a/Server/Pages/ApiKeys.razor b/Server/Pages/ApiKeys.razor index 2386ad500..f773328db 100644 --- a/Server/Pages/ApiKeys.razor +++ b/Server/Pages/ApiKeys.razor @@ -1,5 +1,6 @@ @page "/api-keys" @using Immense.RemoteControl.Shared.Helpers; +@using Remotely.Shared.Entities; @attribute [Authorize(Policy = PolicyNames.OrganizationAdminRequired)] @inherits AuthComponentBase diff --git a/Server/Pages/DeviceDetails.razor.cs b/Server/Pages/DeviceDetails.razor.cs index 830fe90c8..c3668ea4f 100644 --- a/Server/Pages/DeviceDetails.razor.cs +++ b/Server/Pages/DeviceDetails.razor.cs @@ -4,8 +4,8 @@ using Remotely.Server.Components; using Remotely.Server.Hubs; using Remotely.Server.Services; +using Remotely.Shared.Entities; using Remotely.Shared.Enums; -using Remotely.Shared.Models; using Remotely.Shared.Utilities; using System; using System.Collections.Concurrent; diff --git a/Server/Pages/ManageOrganization.razor.cs b/Server/Pages/ManageOrganization.razor.cs index 992813a7f..fbd6b3fbd 100644 --- a/Server/Pages/ManageOrganization.razor.cs +++ b/Server/Pages/ManageOrganization.razor.cs @@ -7,6 +7,7 @@ using Remotely.Server.Components; using Remotely.Server.Components.ModalContents; using Remotely.Server.Services; +using Remotely.Shared.Entities; using Remotely.Shared.Models; using Remotely.Shared.ViewModels; using System; diff --git a/Server/Pages/ServerConfig.razor.cs b/Server/Pages/ServerConfig.razor.cs index c6d7f6945..dec2fb5d3 100644 --- a/Server/Pages/ServerConfig.razor.cs +++ b/Server/Pages/ServerConfig.razor.cs @@ -9,8 +9,8 @@ using Remotely.Server.Components; using Remotely.Server.Hubs; using Remotely.Server.Services; +using Remotely.Shared.Entities; using Remotely.Shared.Enums; -using Remotely.Shared.Models; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; diff --git a/Server/Pages/Shared/_Layout.cshtml b/Server/Pages/Shared/_Layout.cshtml index 535b01ae4..eef7464a7 100644 --- a/Server/Pages/Shared/_Layout.cshtml +++ b/Server/Pages/Shared/_Layout.cshtml @@ -3,6 +3,7 @@ @using Microsoft.EntityFrameworkCore; @using Remotely.Server.Services @using Remotely.Shared.Models +@using Remotely.Shared.Entities @inject IApplicationConfig AppConfig @inject IWebHostEnvironment Environment @inject ICompositeViewEngine Engine diff --git a/Server/Pages/Shared/_LoginPartial.cshtml b/Server/Pages/Shared/_LoginPartial.cshtml index 5edd244a1..c5ef3193c 100644 --- a/Server/Pages/Shared/_LoginPartial.cshtml +++ b/Server/Pages/Shared/_LoginPartial.cshtml @@ -1,6 +1,6 @@ @using Microsoft.AspNetCore.Identity @using Remotely.Shared.Models - +@using Remotely.Shared.Entities @inject SignInManager SignInManager @inject UserManager UserManager @inject Remotely.Server.Services.IApplicationConfig AppConfig diff --git a/Server/Pages/_Host.cshtml b/Server/Pages/_Host.cshtml index 26289ed64..4f1f8f8d1 100644 --- a/Server/Pages/_Host.cshtml +++ b/Server/Pages/_Host.cshtml @@ -1,6 +1,7 @@ @page "/" @using Remotely.Server.Services @using Remotely.Shared.Models +@using Remotely.Shared.Entities @namespace Remotely.Server.Pages @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @inject IDataService DataService diff --git a/Server/Program.cs b/Server/Program.cs index ec6b1b78c..b5de741d2 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -23,7 +23,6 @@ using Remotely.Server.Data; using Remotely.Server.Hubs; using Remotely.Server.Services; -using Remotely.Shared.Models; using System.IO; using System.Linq; using System.Net; @@ -39,6 +38,7 @@ using Nihs.SimpleMessenger; using Microsoft.AspNetCore.RateLimiting; using RatePolicyNames = Remotely.Server.RateLimiting.PolicyNames; +using Remotely.Shared.Entities; var builder = WebApplication.CreateBuilder(args); var configuration = builder.Configuration; diff --git a/Server/Services/AgentHubSessionCache.cs b/Server/Services/AgentHubSessionCache.cs index 4ad82cbdf..4921d39f7 100644 --- a/Server/Services/AgentHubSessionCache.cs +++ b/Server/Services/AgentHubSessionCache.cs @@ -1,5 +1,5 @@ using Immense.RemoteControl.Server.Models; -using Remotely.Shared.Models; +using Remotely.Shared.Entities; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; diff --git a/Server/Services/AuthService.cs b/Server/Services/AuthService.cs index 7cc451d02..4884dcc60 100644 --- a/Server/Services/AuthService.cs +++ b/Server/Services/AuthService.cs @@ -1,7 +1,7 @@ using Immense.RemoteControl.Shared; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Identity; -using Remotely.Shared.Models; +using Remotely.Shared.Entities; using System; using System.Collections.Generic; using System.Linq; diff --git a/Server/Services/DataService.cs b/Server/Services/DataService.cs index 309f592f1..3d1123618 100644 --- a/Server/Services/DataService.cs +++ b/Server/Services/DataService.cs @@ -12,6 +12,7 @@ using Remotely.Server.Models; using Remotely.Shared; using Remotely.Shared.Dtos; +using Remotely.Shared.Entities; using Remotely.Shared.Enums; using Remotely.Shared.Models; using Remotely.Shared.Utilities; diff --git a/Server/Services/ScriptScheduleDispatcher.cs b/Server/Services/ScriptScheduleDispatcher.cs index 7cf4bc500..544f6af5d 100644 --- a/Server/Services/ScriptScheduleDispatcher.cs +++ b/Server/Services/ScriptScheduleDispatcher.cs @@ -1,8 +1,8 @@ using Immense.RemoteControl.Server.Abstractions; using Microsoft.Extensions.Logging; using Remotely.Server.Hubs; +using Remotely.Shared.Entities; using Remotely.Shared.Enums; -using Remotely.Shared.Models; using Remotely.Shared.Utilities; using System; using System.Collections.Generic; diff --git a/Server/_Imports.razor b/Server/_Imports.razor index aca46d5e3..d5b02e4e9 100644 --- a/Server/_Imports.razor +++ b/Server/_Imports.razor @@ -23,4 +23,5 @@ @using System.Collections.Concurrent @using Remotely.Server.Components.Scripts @using Remotely.Server.Components.TreeView -@using Remotely.Server.Auth \ No newline at end of file +@using Remotely.Server.Auth +@using Remotely.Shared.Entities \ No newline at end of file diff --git a/Shared/Models/Alert.cs b/Shared/Entities/Alert.cs similarity index 95% rename from Shared/Models/Alert.cs rename to Shared/Entities/Alert.cs index 151007577..2f8a1b414 100644 --- a/Shared/Models/Alert.cs +++ b/Shared/Entities/Alert.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Text.Json.Serialization; -namespace Remotely.Shared.Models; +namespace Remotely.Shared.Entities; public class Alert { diff --git a/Shared/Models/ApiToken.cs b/Shared/Entities/ApiToken.cs similarity index 94% rename from Shared/Models/ApiToken.cs rename to Shared/Entities/ApiToken.cs index 86c7448c8..c37b58e68 100644 --- a/Shared/Models/ApiToken.cs +++ b/Shared/Entities/ApiToken.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Text.Json.Serialization; -namespace Remotely.Shared.Models; +namespace Remotely.Shared.Entities; public class ApiToken { diff --git a/Shared/Models/BrandingInfo.cs b/Shared/Entities/BrandingInfo.cs similarity index 78% rename from Shared/Models/BrandingInfo.cs rename to Shared/Entities/BrandingInfo.cs index 52354efd0..0e8d64d67 100644 --- a/Shared/Models/BrandingInfo.cs +++ b/Shared/Entities/BrandingInfo.cs @@ -9,15 +9,15 @@ using System.Text.Json.Serialization; using System.Threading.Tasks; -namespace Remotely.Shared.Models; +namespace Remotely.Shared.Entities; public class BrandingInfo : BrandingInfoBase { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public string Id { get; set; } = string.Empty; + public string Id { get; set; } = null!; - public string? OrganizationId { get; set; } + public string OrganizationId { get; set; } = null!; [JsonIgnore] public Organization? Organization { get; set; } diff --git a/Shared/Models/Device.cs b/Shared/Entities/Device.cs similarity index 95% rename from Shared/Models/Device.cs rename to Shared/Entities/Device.cs index 3720bad53..e37e74c3d 100644 --- a/Shared/Models/Device.cs +++ b/Shared/Entities/Device.cs @@ -1,5 +1,6 @@ using Remotely.Shared.Attributes; using Remotely.Shared.Enums; +using Remotely.Shared.Models; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -7,7 +8,7 @@ using System.Runtime.InteropServices; using System.Text.Json.Serialization; -namespace Remotely.Shared.Models; +namespace Remotely.Shared.Entities; public class Device { @@ -40,7 +41,7 @@ public class Device public List? Drives { get; set; } [Key] - public string ID { get; set; } = null!; + public string ID { get; set; } = Guid.NewGuid().ToString(); public bool Is64Bit { get; set; } public bool IsOnline { get; set; } @@ -54,7 +55,7 @@ public class Device public string[] MacAddresses { get; set; } = Array.Empty(); [StringLength(5000)] - public string? Notes { get; set; } + public string? Notes { get; set; } [JsonIgnore] public Organization? Organization { get; set; } diff --git a/Shared/Models/InviteLink.cs b/Shared/Entities/InviteLink.cs similarity index 85% rename from Shared/Models/InviteLink.cs rename to Shared/Entities/InviteLink.cs index 712dd7558..12324acf3 100644 --- a/Shared/Models/InviteLink.cs +++ b/Shared/Entities/InviteLink.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Text.Json.Serialization; -namespace Remotely.Shared.Models; +namespace Remotely.Shared.Entities; public class InviteLink { @@ -15,6 +15,6 @@ public class InviteLink public DateTimeOffset DateSent { get; set; } [JsonIgnore] public Organization? Organization { get; set; } - public string? OrganizationID { get; set; } + public string OrganizationID { get; set; } = null!; public string? ResetUrl { get; set; } } diff --git a/Shared/Models/Organization.cs b/Shared/Entities/Organization.cs similarity index 95% rename from Shared/Models/Organization.cs rename to Shared/Entities/Organization.cs index 610226c7e..8ec7b6da0 100644 --- a/Shared/Models/Organization.cs +++ b/Shared/Entities/Organization.cs @@ -1,11 +1,12 @@ using Immense.RemoteControl.Shared.Models; using Remotely.Shared.Enums; +using Remotely.Shared.Models; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -namespace Remotely.Shared.Models; +namespace Remotely.Shared.Entities; public class Organization { diff --git a/Shared/Models/RemotelyUser.cs b/Shared/Entities/RemotelyUser.cs similarity index 92% rename from Shared/Models/RemotelyUser.cs rename to Shared/Entities/RemotelyUser.cs index 53c863c6b..62c1c2902 100644 --- a/Shared/Models/RemotelyUser.cs +++ b/Shared/Entities/RemotelyUser.cs @@ -1,9 +1,10 @@ using Microsoft.AspNetCore.Identity; +using Remotely.Shared.Models; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; -namespace Remotely.Shared.Models; +namespace Remotely.Shared.Entities; public class RemotelyUser : IdentityUser { diff --git a/Shared/Models/SavedScript.cs b/Shared/Entities/SavedScript.cs similarity index 97% rename from Shared/Models/SavedScript.cs rename to Shared/Entities/SavedScript.cs index 2d91fedb0..7b4b5a7bc 100644 --- a/Shared/Models/SavedScript.cs +++ b/Shared/Entities/SavedScript.cs @@ -8,7 +8,7 @@ using System.Text.Json.Serialization; using System.Threading.Tasks; -namespace Remotely.Shared.Models; +namespace Remotely.Shared.Entities; public class SavedScript { diff --git a/Shared/Models/ScriptResult.cs b/Shared/Entities/ScriptResult.cs similarity index 84% rename from Shared/Models/ScriptResult.cs rename to Shared/Entities/ScriptResult.cs index 54db01c0f..0dd8a7402 100644 --- a/Shared/Models/ScriptResult.cs +++ b/Shared/Entities/ScriptResult.cs @@ -8,7 +8,7 @@ using System.Runtime.Serialization; using System.Text.Json.Serialization; -namespace Remotely.Shared.Models; +namespace Remotely.Shared.Entities; public class ScriptResult : ScriptResultBase { @@ -17,12 +17,12 @@ public class ScriptResult : ScriptResultBase [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public string ID { get; set; } = string.Empty; + public string ID { get; set; } = null!; [JsonIgnore] [IgnoreDataMember] public Organization? Organization { get; set; } - public string OrganizationID { get; set; } = string.Empty; + public required string OrganizationID { get; set; } [JsonIgnore] public SavedScript? SavedScript { get; set; } diff --git a/Shared/Models/ScriptRun.cs b/Shared/Entities/ScriptRun.cs similarity index 96% rename from Shared/Models/ScriptRun.cs rename to Shared/Entities/ScriptRun.cs index 90c610241..9889a63b5 100644 --- a/Shared/Models/ScriptRun.cs +++ b/Shared/Entities/ScriptRun.cs @@ -8,7 +8,7 @@ using System.Text.Json.Serialization; using System.Threading.Tasks; -namespace Remotely.Shared.Models; +namespace Remotely.Shared.Entities; public class ScriptRun { diff --git a/Shared/Models/ScriptSchedule.cs b/Shared/Entities/ScriptSchedule.cs similarity index 95% rename from Shared/Models/ScriptSchedule.cs rename to Shared/Entities/ScriptSchedule.cs index 71cf7e8cb..63f64664d 100644 --- a/Shared/Models/ScriptSchedule.cs +++ b/Shared/Entities/ScriptSchedule.cs @@ -1,4 +1,5 @@ using Remotely.Shared.Enums; +using Remotely.Shared.Models; using Remotely.Shared.Utilities; using System; using System.Collections.Generic; @@ -9,7 +10,7 @@ using System.Text.Json.Serialization; using System.Threading.Tasks; -namespace Remotely.Shared.Models; +namespace Remotely.Shared.Entities; public class ScriptSchedule { diff --git a/Shared/Models/SharedFile.cs b/Shared/Entities/SharedFile.cs similarity index 94% rename from Shared/Models/SharedFile.cs rename to Shared/Entities/SharedFile.cs index 72fed5b84..ec69176e4 100644 --- a/Shared/Models/SharedFile.cs +++ b/Shared/Entities/SharedFile.cs @@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -namespace Remotely.Shared.Models; +namespace Remotely.Shared.Entities; public class SharedFile { diff --git a/Shared/Extensions/DeviceExtensions.cs b/Shared/Extensions/DeviceExtensions.cs index 87901f188..8997c1281 100644 --- a/Shared/Extensions/DeviceExtensions.cs +++ b/Shared/Extensions/DeviceExtensions.cs @@ -1,5 +1,5 @@ using Remotely.Shared.Dtos; -using Remotely.Shared.Models; +using Remotely.Shared.Entities; using System; using System.Collections.Generic; using System.Linq; diff --git a/Shared/Models/DeviceGroup.cs b/Shared/Models/DeviceGroup.cs index 611d4d305..a04d55b1c 100644 --- a/Shared/Models/DeviceGroup.cs +++ b/Shared/Models/DeviceGroup.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Text.Json.Serialization; +using Remotely.Shared.Entities; namespace Remotely.Shared.Models; diff --git a/Shared/Shared.csproj b/Shared/Shared.csproj index 182ada766..7d0f55925 100644 --- a/Shared/Shared.csproj +++ b/Shared/Shared.csproj @@ -22,11 +22,11 @@ - + ScriptResult.cs.d.ts DtsGenerator - + diff --git a/Tests/Server.Tests/DataServiceTests.cs b/Tests/Server.Tests/DataServiceTests.cs index 003f7676e..245ed3fe4 100644 --- a/Tests/Server.Tests/DataServiceTests.cs +++ b/Tests/Server.Tests/DataServiceTests.cs @@ -3,6 +3,7 @@ using Moq; using Remotely.Server.Services; using Remotely.Shared.Dtos; +using Remotely.Shared.Entities; using Remotely.Shared.Models; using Remotely.Shared.Utilities; using System; diff --git a/Tests/Server.Tests/IoCActivator.cs b/Tests/Server.Tests/IoCActivator.cs index 7c0e84f65..41c67c6f0 100644 --- a/Tests/Server.Tests/IoCActivator.cs +++ b/Tests/Server.Tests/IoCActivator.cs @@ -10,7 +10,7 @@ using Remotely.Server.API; using Remotely.Server.Data; using Remotely.Server.Services; -using Remotely.Shared.Models; +using Remotely.Shared.Entities; using Remotely.Shared.Utilities; using System; using System.Collections.Generic; diff --git a/Tests/Server.Tests/ScriptScheduleDispatcherTests.cs b/Tests/Server.Tests/ScriptScheduleDispatcherTests.cs index be1d0f926..55a0fefd8 100644 --- a/Tests/Server.Tests/ScriptScheduleDispatcherTests.cs +++ b/Tests/Server.Tests/ScriptScheduleDispatcherTests.cs @@ -5,6 +5,7 @@ using Moq; using Remotely.Server.Hubs; using Remotely.Server.Services; +using Remotely.Shared.Entities; using Remotely.Shared.Enums; using Remotely.Shared.Models; using Remotely.Shared.Utilities; diff --git a/Tests/Server.Tests/TestData.cs b/Tests/Server.Tests/TestData.cs index 14089fa6b..32014e744 100644 --- a/Tests/Server.Tests/TestData.cs +++ b/Tests/Server.Tests/TestData.cs @@ -6,6 +6,7 @@ using Remotely.Server.Data; using Remotely.Server.Services; using Remotely.Shared.Dtos; +using Remotely.Shared.Entities; using Remotely.Shared.Models; using System; using System.Linq; From a0807cf26bdb05d1a3a6c5494ce1cd446f8fe254 Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Thu, 27 Jul 2023 09:49:23 -0700 Subject: [PATCH 19/29] Fix AuthComponentBase and tests. --- Server/Components/AuthComponentBase.cs | 58 ++++++++++++++----- Server/Components/AuthorizedIndex.razor | 5 +- .../Scripts/ScriptSchedules.razor.cs | 8 +-- Server/Components/TabControl/TabHeader.razor | 8 +-- Server/Hubs/CircuitConnection.cs | 21 ++++++- Server/Pages/DeviceDetails.razor.cs | 5 -- Server/Services/DataService.cs | 1 + Tests/Server.Tests/DataServiceTests.cs | 6 +- 8 files changed, 77 insertions(+), 35 deletions(-) diff --git a/Server/Components/AuthComponentBase.cs b/Server/Components/AuthComponentBase.cs index 371392a6c..42b922ed1 100644 --- a/Server/Components/AuthComponentBase.cs +++ b/Server/Components/AuthComponentBase.cs @@ -1,47 +1,73 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; using Remotely.Server.Services; using Remotely.Shared.Entities; using System; -using System.Diagnostics.CodeAnalysis; +using System.Threading; using System.Threading.Tasks; namespace Remotely.Server.Components; +[Authorize] public class AuthComponentBase : ComponentBase { + private readonly ManualResetEventSlim _initSignal = new(); private RemotelyUser? _user; private string? _userName; - protected override async Task OnInitializedAsync() - { - IsAuthenticated = await AuthService.IsAuthenticated(); - var userResult = await AuthService.GetUser(); - if (userResult.IsSuccess) - { - _user = userResult.Value; - _userName = userResult.Value.UserName ?? string.Empty; - } - await base.OnInitializedAsync(); - } - public bool IsAuthenticated { get; private set; } public bool IsUserSet => _user is not null; public RemotelyUser User { - get => _user ?? throw new InvalidOperationException("User has not been resolved yet."); + get + { + if (_initSignal.Wait(TimeSpan.FromSeconds(5)) && _user is not null) + { + return _user; + } + // This should never happen, since AuthBasedComponent is only + // used on components that require authentication. This was easier + // than making this explicitly nullable and refactoring everywhere. + Logger.LogError("Failed to resolve user."); + throw new InvalidOperationException("Failed to resolve user."); + } private set => _user = value; } public string UserName { - get => _userName ?? throw new InvalidOperationException("User has not been resolved yet."); + get + { + if (_initSignal.Wait(TimeSpan.FromSeconds(5)) && _userName is not null) + { + return _userName; + } + Logger.LogError("Failed to resolve user."); + throw new InvalidOperationException("Failed to resolve user."); + } private set => _userName = value; } [Inject] protected IAuthService AuthService { get; set; } = null!; + + [Inject] + private ILogger Logger { get; init; } = null!; + + + protected override async Task OnInitializedAsync() + { + IsAuthenticated = await AuthService.IsAuthenticated(); + var userResult = await AuthService.GetUser(); + if (userResult.IsSuccess) + { + _user = userResult.Value; + _userName = userResult.Value.UserName ?? string.Empty; + } + _initSignal.Set(); + await base.OnInitializedAsync(); + } } diff --git a/Server/Components/AuthorizedIndex.razor b/Server/Components/AuthorizedIndex.razor index b8a8fa586..ba0e9936c 100644 --- a/Server/Components/AuthorizedIndex.razor +++ b/Server/Components/AuthorizedIndex.razor @@ -20,7 +20,7 @@ protected override void OnAfterRender(bool firstRender) { - if (AppConfig.Require2FA && IsUserSet && !User.TwoFactorEnabled) + if (AppConfig.Require2FA && !User.TwoFactorEnabled) { NavManager.NavigateTo("/TwoFactorRequired"); } @@ -31,6 +31,9 @@ { await base.OnInitializedAsync(); + // This handles a weird edge case when the user has been + // deleted but still has an authentication cookie in their + // browser. if (IsAuthenticated == true && !IsUserSet) { await SignInManager.SignOutAsync(); diff --git a/Server/Components/Scripts/ScriptSchedules.razor.cs b/Server/Components/Scripts/ScriptSchedules.razor.cs index dacdb86e6..c7a2b9427 100644 --- a/Server/Components/Scripts/ScriptSchedules.razor.cs +++ b/Server/Components/Scripts/ScriptSchedules.razor.cs @@ -50,12 +50,12 @@ public partial class ScriptSchedules : AuthComponentBase private IToastService ToastService { get; set; } = null!; private bool CanModifySchedule => - _selectedSchedule.CreatorId == User?.Id || - User?.IsAdministrator == true; + _selectedSchedule.CreatorId == User.Id || + User.IsAdministrator; private bool CanDeleteSchedule => - _selectedSchedule.CreatorId == User?.Id || - User?.IsAdministrator == true; + _selectedSchedule.CreatorId == User.Id || + User.IsAdministrator; protected override async Task OnInitializedAsync() { diff --git a/Server/Components/TabControl/TabHeader.razor b/Server/Components/TabControl/TabHeader.razor index 80238cd3c..a21545c7f 100644 --- a/Server/Components/TabControl/TabHeader.razor +++ b/Server/Components/TabControl/TabHeader.razor @@ -32,13 +32,13 @@ throw new Exception("TabHeader must be contained in a TabControl."); } - if (!string.IsNullOrWhiteSpace(NavigationUri)) + if (Parent.ActiveTab == Name) { OnActivated?.Invoke(); } } - - private void SetActiveTab() + + private async Task SetActiveTab() { if (!string.IsNullOrWhiteSpace(NavigationUri)) { @@ -47,7 +47,7 @@ else { Parent?.SetActiveTab(this); - StateHasChanged(); + await InvokeAsync(StateHasChanged); OnActivated?.Invoke(); } } diff --git a/Server/Hubs/CircuitConnection.cs b/Server/Hubs/CircuitConnection.cs index 1bc1bc609..b37b564dc 100644 --- a/Server/Hubs/CircuitConnection.cs +++ b/Server/Hubs/CircuitConnection.cs @@ -85,6 +85,7 @@ public class CircuitConnection : CircuitHandler, ICircuitConnection private readonly ILogger _logger; private readonly IAgentHubSessionCache _agentSessionCache; private readonly IToastService _toastService; + private readonly ManualResetEventSlim _initSignal = new(); private RemotelyUser? _user; public CircuitConnection( @@ -120,8 +121,23 @@ public CircuitConnection( public RemotelyUser User { - get => _user ?? throw new InvalidOperationException("User has not been resolved yet."); - internal set => _user = value; + get + { + if (_initSignal.Wait(TimeSpan.FromSeconds(5)) && _user is not null) + { + return _user; + } + _logger.LogError("Failed to resolve user."); + throw new InvalidOperationException("Failed to resolve user."); + } + internal set + { + _user = value; + if (_user is not null) + { + _initSignal.Set(); + } + } } @@ -223,6 +239,7 @@ public override async Task OnCircuitOpenedAsync(Circuit circuit, CancellationTok } _user = userResult.Value; _circuitManager.TryAddConnection(ConnectionId, this); + _initSignal.Set(); } await base.OnCircuitOpenedAsync(circuit, cancellationToken); } diff --git a/Server/Pages/DeviceDetails.razor.cs b/Server/Pages/DeviceDetails.razor.cs index c3668ea4f..3de0b5a5c 100644 --- a/Server/Pages/DeviceDetails.razor.cs +++ b/Server/Pages/DeviceDetails.razor.cs @@ -171,11 +171,6 @@ private string GetTrimmedText(string source, int stringLength) return source[0..25] + "..."; } - private string GetTrimmedText(string[] source, int stringLength) - { - return GetTrimmedText(string.Join("", source), stringLength); - } - private Task HandleValidSubmit() { if (_device is null) diff --git a/Server/Services/DataService.cs b/Server/Services/DataService.cs index 3d1123618..9d3106fc9 100644 --- a/Server/Services/DataService.cs +++ b/Server/Services/DataService.cs @@ -1566,6 +1566,7 @@ public async Task> GetPendingScriptRuns(string deviceId) .AsNoTracking() .Include(x => x.ScriptRuns) .ThenInclude(x => x.Results) + .Include(x => x.ScriptResults) .FirstOrDefaultAsync(x => x.ID == deviceId); if (device is null) diff --git a/Tests/Server.Tests/DataServiceTests.cs b/Tests/Server.Tests/DataServiceTests.cs index 245ed3fe4..724b8fcc1 100644 --- a/Tests/Server.Tests/DataServiceTests.cs +++ b/Tests/Server.Tests/DataServiceTests.cs @@ -66,12 +66,12 @@ public async Task CreateDevice() }; // First call should create and return device. - var savedDevice = await _dataService.CreateDevice(deviceOptions); + var savedDevice = (await _dataService.CreateDevice(deviceOptions)).Value!; Assert.IsInstanceOfType(savedDevice, typeof(Device)); - // Second call with same DeviceUuid should return null; + // Second call with same DeviceUuid should fail. var secondSave = await _dataService.CreateDevice(deviceOptions); - Assert.IsNull(secondSave); + Assert.IsFalse(secondSave.IsSuccess); } [TestMethod] From d0e6bce99a6f10e176f59a98eb336efdbf2fced4 Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Thu, 27 Jul 2023 10:13:46 -0700 Subject: [PATCH 20/29] Fix string interpolation in queries. --- Server/Services/DataService.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Server/Services/DataService.cs b/Server/Services/DataService.cs index 9d3106fc9..8bd821621 100644 --- a/Server/Services/DataService.cs +++ b/Server/Services/DataService.cs @@ -600,7 +600,7 @@ public bool AddUserToDeviceGroup(string orgId, string groupId, string userName, var user = dbContext.Users .Include(x => x.DeviceGroups) .FirstOrDefault(x => - $"{x.UserName}".ToLower() == userName && + x.UserName!.ToLower() == userName && x.OrganizationID == orgId); if (user == null) @@ -997,7 +997,9 @@ public bool DoesUserExist(string userName) return false; } - return dbContext.Users.Any(x => $"{x.UserName}".Trim().ToLower() == userName.Trim().ToLower()); + return dbContext.Users + .Where(x => x.UserName != null) + .Any(x => x.UserName!.Trim().ToLower() == userName.Trim().ToLower()); } public bool DoesUserHaveAccessToDevice(string deviceId, RemotelyUser remotelyUser) @@ -1417,7 +1419,6 @@ public DeviceGroup[] GetDeviceGroupsForOrganization(string organizationId) return dbContext.DeviceGroups .AsNoTracking() .Include(x => x.Users) - .ThenInclude(x => x.DeviceGroups) .Where(x => x.OrganizationID == organizationId) .OrderBy(x => x.Name) .ToArray(); @@ -1715,8 +1716,8 @@ public List GetServerAdmins() return dbContext.Users .AsNoTracking() - .Where(x => x.IsServerAdmin) - .Select(x => $"{x.UserName}") + .Where(x => x.IsServerAdmin && x.UserName != null) + .Select(x => x.UserName!) .ToList(); } From afc4f6e49c3fc5bffe4479fe20ec2ab2126f3918 Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Thu, 27 Jul 2023 11:17:37 -0700 Subject: [PATCH 21/29] Fix/improve log streaming from agent. --- Agent/Program.cs | 9 ++- Agent/Services/AgentHubConnection.cs | 65 ++++++-------------- Agent/Services/FileLogsManager.cs | 85 +++++++++++++++++++++++++++ Shared/Services/FileLogger.cs | 82 ++------------------------ Shared/Services/FileLoggerDefaults.cs | 56 ++++++++++++++++++ 5 files changed, 170 insertions(+), 127 deletions(-) create mode 100644 Agent/Services/FileLogsManager.cs create mode 100644 Shared/Services/FileLoggerDefaults.cs diff --git a/Agent/Program.cs b/Agent/Program.cs index 7afc93ee5..d6a8b3eb1 100644 --- a/Agent/Program.cs +++ b/Agent/Program.cs @@ -17,6 +17,7 @@ using Microsoft.Extensions.Hosting; using System.Linq; using Microsoft.Win32; +using System.Reflection; namespace Remotely.Agent; @@ -47,7 +48,8 @@ public static async Task Main(string[] args) catch (Exception ex) { var version = AppVersionHelper.GetAppVersion(); - var logger = new FileLogger("Remotely_Agent", version, "Main"); + var componentName = Assembly.GetExecutingAssembly().GetName().Name; + var logger = new FileLogger($"{componentName}", version, "Main"); logger.LogError(ex, "Error during agent startup."); throw; } @@ -86,10 +88,10 @@ private static void RegisterServices(IServiceCollection services) { builder.AddConsole().AddDebug(); var version = AppVersionHelper.GetAppVersion(); - builder.AddProvider(new FileLoggerProvider("Remotely_Agent", version)); + var componentName = Assembly.GetExecutingAssembly().GetName().Name; + builder.AddProvider(new FileLoggerProvider($"{componentName}", version)); }); - // TODO: All these should be registered as interfaces. services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -102,6 +104,7 @@ private static void RegisterServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddSingleton(); if (OperatingSystem.IsWindows()) { diff --git a/Agent/Services/AgentHubConnection.cs b/Agent/Services/AgentHubConnection.cs index e68a2683a..2e759d1d3 100644 --- a/Agent/Services/AgentHubConnection.cs +++ b/Agent/Services/AgentHubConnection.cs @@ -39,7 +39,7 @@ public class AgentHubConnection : IAgentHubConnection, IDisposable private readonly IHttpClientFactory _httpFactory; private readonly IWakeOnLanService _wakeOnLanService; private readonly ILogger _logger; - private readonly IEnumerable _loggerProviders; + private readonly IFileLogsManager _fileLogsManager; private readonly IScriptExecutor _scriptExecutor; private readonly IUninstaller _uninstaller; private readonly IUpdater _updater; @@ -48,7 +48,6 @@ public class AgentHubConnection : IAgentHubConnection, IDisposable private HubConnection? _hubConnection; private Timer? _heartbeatTimer; private bool _isServerVerified; - private FileLogger? _fileLogger; public AgentHubConnection( IConfigService configService, @@ -60,7 +59,7 @@ public AgentHubConnection( IDeviceInformationService deviceInfoService, IHttpClientFactory httpFactory, IWakeOnLanService wakeOnLanService, - IEnumerable loggerProviders, + IFileLogsManager fileLogsManager, ILogger logger) { _configService = configService; @@ -73,7 +72,7 @@ public AgentHubConnection( _httpFactory = httpFactory; _wakeOnLanService = wakeOnLanService; _logger = logger; - _loggerProviders = loggerProviders; + _fileLogsManager = fileLogsManager; } public bool IsConnected => _hubConnection?.State == HubConnectionState.Connected; @@ -303,14 +302,7 @@ private void RegisterMessageHandlers() _hubConnection.On("DeleteLogs", () => { - if (TryGetFileLogger(out var fileLogger)) - { - fileLogger.DeleteLogs(); - } - if (_fileLogger is FileLogger logger) - { - logger.DeleteLogs(); - } + _fileLogsManager.DeleteLogs(); }); @@ -373,26 +365,24 @@ private void RegisterMessageHandlers() _hubConnection.On("GetLogs", async (string senderConnectionId) => { - if (_fileLogger is not FileLogger logger) - { - await _hubConnection.InvokeAsync("SendLogs", "Logger is not of expected type.", senderConnectionId).ConfigureAwait(false); - return; - } - - var logBytes = await logger.ReadAllBytes(); - - if (!logBytes.Any()) + try { - var message = "There are no log entries written."; + if (!await _fileLogsManager.AnyLogsExist()) + { + var message = "There are no log entries written."; + await _hubConnection.InvokeAsync("SendLogs", message, senderConnectionId).ConfigureAwait(false); + return; + } - await _hubConnection.InvokeAsync("SendLogs", message, senderConnectionId).ConfigureAwait(false); - return; + await foreach (var chunk in _fileLogsManager.ReadAllBytes()) + { + var lines = Encoding.UTF8.GetString(chunk); + await _hubConnection.InvokeAsync("SendLogs", lines, senderConnectionId).ConfigureAwait(false); + } } - - for (var i = 0; i < logBytes.Length; i += 50_000) + catch (Exception ex) { - var chunk = Encoding.UTF8.GetString(logBytes.Skip(i).Take(50_000).ToArray()); - await _hubConnection.InvokeAsync("SendLogs", chunk, senderConnectionId).ConfigureAwait(false); + _logger.LogError(ex, "Error while retrieving logs."); } }); @@ -544,25 +534,6 @@ private void RegisterMessageHandlers() }); } - private bool TryGetFileLogger([NotNullWhen(true)] out FileLogger? fileLogger) - { - if (_fileLogger is null) - { - var logger = _loggerProviders - .OfType() - .FirstOrDefault() - ?.CreateLogger(nameof(AgentHubConnection)); - - if (logger is FileLogger loggerImpl) - { - _fileLogger = loggerImpl; - } - } - - fileLogger = _fileLogger; - return fileLogger is not null; - } - private async Task VerifyServer() { if (_connectionInfo is null || _hubConnection is null) diff --git a/Agent/Services/FileLogsManager.cs b/Agent/Services/FileLogsManager.cs new file mode 100644 index 000000000..a9684b631 --- /dev/null +++ b/Agent/Services/FileLogsManager.cs @@ -0,0 +1,85 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace Remotely.Shared.Services; + +public interface IFileLogsManager +{ + Task AnyLogsExist(); + Task DeleteLogs(); + IAsyncEnumerable ReadAllBytes(); +} + +public class FileLogsManager : IFileLogsManager +{ + public async Task AnyLogsExist() + { + using var logLock = await FileLoggerDefaults.AcquireLock(); + + var componentName = Assembly.GetExecutingAssembly().GetName().Name; + var directory = Path.Combine(FileLoggerDefaults.LogsFolderPath, $"{componentName}"); + + if (Directory.Exists(directory)) + { + foreach (var file in Directory.GetFiles(directory)) + { + if (new FileInfo(file).Length > 0) + { + return true; + } + } + } + return false; + } + + public async Task DeleteLogs() + { + using var logLock = await FileLoggerDefaults.AcquireLock(); + + var componentName = Assembly.GetExecutingAssembly().GetName().Name; + var directory = Path.Combine(FileLoggerDefaults.LogsFolderPath, $"{componentName}"); + + if (Directory.Exists(directory)) + { + foreach (var file in Directory.GetFiles(directory)) + { + try + { + File.Delete(file); + } + catch { } + } + } + } + + public async IAsyncEnumerable ReadAllBytes() + { + using var logLock = await FileLoggerDefaults.AcquireLock(); + + var componentName = Assembly.GetExecutingAssembly().GetName().Name; + var directory = Path.Combine(FileLoggerDefaults.LogsFolderPath, $"{componentName}"); + + if (!Directory.Exists(directory)) + { + yield break; + } + + var files = Directory + .GetFiles(directory) + .OrderBy(File.GetCreationTime); + + foreach (var file in files) + { + foreach (var chunk in File.ReadAllBytes(file).Chunk(50_000)) + { + yield return File.ReadAllBytes(file); + } + } + } +} diff --git a/Shared/Services/FileLogger.cs b/Shared/Services/FileLogger.cs index 3cc376a97..4f4c10671 100644 --- a/Shared/Services/FileLogger.cs +++ b/Shared/Services/FileLogger.cs @@ -14,7 +14,6 @@ namespace Remotely.Shared.Services; public class FileLogger : ILogger { private static readonly ConcurrentStack _scopeStack = new(); - private static readonly SemaphoreSlim _writeLock = new(1, 1); private readonly string _categoryName; private readonly string _componentName; private readonly string _componentVersion; @@ -27,62 +26,15 @@ public FileLogger(string componentName, string componentVersion, string category _categoryName = categoryName; } - private static string LogsFolderPath - { - get - { - if (OperatingSystem.IsWindows()) - { - var logsPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), - "Remotely", - "Logs"); - - if (EnvironmentHelper.IsDebug) - { - logsPath += "_Debug"; - } - return logsPath; - } - - if (OperatingSystem.IsLinux()) - { - if (EnvironmentHelper.IsDebug) - { - return "/var/log/remotely_debug"; - } - return "/var/log/remotely"; - } - - throw new PlatformNotSupportedException(); - } - } - private string LogPath => Path.Combine(LogsFolderPath, _componentName, $"LogFile_{DateTime.Now:yyyy-MM-dd}.log"); + private string LogPath => FileLoggerDefaults.GetLogPath(_componentName); - public IDisposable? BeginScope(TState state) - where TState : notnull + public IDisposable? BeginScope(TState state) + where TState : notnull { _scopeStack.Push($"{state}"); return new NoopDisposable(); } - public void DeleteLogs() - { - try - { - _writeLock.Wait(); - - if (File.Exists(LogPath)) - { - File.Delete(LogPath); - } - } - catch { } - finally - { - _writeLock.Release(); - } - } public bool IsEnabled(LogLevel logLevel) { @@ -93,10 +45,9 @@ public bool IsEnabled(LogLevel logLevel) _ => false, }; } - - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + public async void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { - _writeLock.Wait(); + using var logLock = await FileLoggerDefaults.AcquireLock(); try { @@ -110,32 +61,9 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except { Console.WriteLine($"Error writing log entry: {ex.Message}"); } - finally - { - _writeLock.Release(); - } } - public async Task ReadAllBytes() - { - try - { - _writeLock.Wait(); - CheckLogFileExists(); - - return await File.ReadAllBytesAsync(LogPath); - } - catch (Exception ex) - { - this.LogError(ex, "Error while reading all bytes from logs."); - return Array.Empty(); - } - finally - { - _writeLock.Release(); - } - } private void CheckLogFileExists() { diff --git a/Shared/Services/FileLoggerDefaults.cs b/Shared/Services/FileLoggerDefaults.cs new file mode 100644 index 000000000..f4d68d274 --- /dev/null +++ b/Shared/Services/FileLoggerDefaults.cs @@ -0,0 +1,56 @@ +using Immense.RemoteControl.Shared.Primitives; +using Remotely.Shared.Utilities; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Remotely.Shared.Services; +public static class FileLoggerDefaults +{ + private static readonly SemaphoreSlim _logLock = new(1, 1); + + public static string LogsFolderPath + { + get + { + if (OperatingSystem.IsWindows()) + { + var logsPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), + "Remotely", + "Logs"); + + if (EnvironmentHelper.IsDebug) + { + logsPath += "_Debug"; + } + return logsPath; + } + + if (OperatingSystem.IsLinux()) + { + if (EnvironmentHelper.IsDebug) + { + return "/var/log/remotely_debug"; + } + return "/var/log/remotely"; + } + + throw new PlatformNotSupportedException(); + } + } + + public static async Task AcquireLock(CancellationToken cancellationToken = default) + { + await _logLock.WaitAsync(cancellationToken); + return new CallbackDisposable(() => _logLock.Release()); + } + + public static string GetLogPath(string componentName) => + Path.Combine(LogsFolderPath, componentName, $"LogFile_{DateTime.Now:yyyy-MM-dd}.log"); + +} From 105f811b77315bdd21eec1d1acb2e97660f27b78 Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Thu, 27 Jul 2023 12:16:43 -0700 Subject: [PATCH 22/29] Add migrations. --- ...5818_Enable_NullableReferences.Designer.cs | 1228 ++++++++++++++++ ...0230726225818_Enable_NullableReferences.cs | 896 ++++++++++++ .../PostgreSqlDbContextModelSnapshot.cs | 322 +++-- ...5809_Enable_NullableReferences.Designer.cs | 1230 +++++++++++++++++ ...0230726225809_Enable_NullableReferences.cs | 917 ++++++++++++ .../SqlServerDbContextModelSnapshot.cs | 325 +++-- ...5800_Enable_NullableReferences.Designer.cs | 1224 ++++++++++++++++ ...0230726225800_Enable_NullableReferences.cs | 896 ++++++++++++ .../Sqlite/SqliteDbContextModelSnapshot.cs | 322 +++-- 9 files changed, 6947 insertions(+), 413 deletions(-) create mode 100644 Server/Migrations/PostgreSql/20230726225818_Enable_NullableReferences.Designer.cs create mode 100644 Server/Migrations/PostgreSql/20230726225818_Enable_NullableReferences.cs create mode 100644 Server/Migrations/SqlServer/20230726225809_Enable_NullableReferences.Designer.cs create mode 100644 Server/Migrations/SqlServer/20230726225809_Enable_NullableReferences.cs create mode 100644 Server/Migrations/Sqlite/20230726225800_Enable_NullableReferences.Designer.cs create mode 100644 Server/Migrations/Sqlite/20230726225800_Enable_NullableReferences.cs diff --git a/Server/Migrations/PostgreSql/20230726225818_Enable_NullableReferences.Designer.cs b/Server/Migrations/PostgreSql/20230726225818_Enable_NullableReferences.Designer.cs new file mode 100644 index 000000000..f9866e553 --- /dev/null +++ b/Server/Migrations/PostgreSql/20230726225818_Enable_NullableReferences.Designer.cs @@ -0,0 +1,1228 @@ +// +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("20230726225818_Enable_NullableReferences")] + partial class Enable_NullableReferences + { + /// + 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("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.Entities.Alert", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Details") + .HasColumnType("text"); + + b.Property("DeviceID") + .IsRequired() + .HasColumnType("text"); + + b.Property("Message") + .HasColumnType("text"); + + b.Property("OrganizationID") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserID") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("DeviceID"); + + b.HasIndex("OrganizationID"); + + b.HasIndex("UserID"); + + b.ToTable("Alerts"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.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") + .IsRequired() + .HasColumnType("text"); + + b.Property("Secret") + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("OrganizationID"); + + b.ToTable("ApiTokens"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.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") + .IsRequired() + .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.Entities.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") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasMaxLength(5000) + .HasColumnType("character varying(5000)"); + + b.Property("OSArchitecture") + .HasColumnType("integer"); + + b.Property("OSDescription") + .HasColumnType("text"); + + b.Property("OrganizationID") + .IsRequired() + .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.Entities.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") + .IsRequired() + .HasColumnType("text"); + + b.Property("ResetUrl") + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("OrganizationID"); + + b.ToTable("InviteLinks"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.Organization", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("IsDefaultOrganization") + .HasColumnType("boolean"); + + b.Property("OrganizationName") + .IsRequired() + .HasMaxLength(25) + .HasColumnType("character varying(25)"); + + b.HasKey("ID"); + + b.ToTable("Organizations"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.SavedScript", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Content") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatorId") + .IsRequired() + .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") + .IsRequired() + .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.Entities.ScriptResult", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("DeviceID") + .IsRequired() + .HasColumnType("text"); + + b.Property("ErrorOutput") + .HasColumnType("text"); + + b.Property("HadErrors") + .HasColumnType("boolean"); + + b.Property("InputType") + .HasColumnType("integer"); + + b.Property("OrganizationID") + .IsRequired() + .HasColumnType("text"); + + b.Property("RunTime") + .HasColumnType("interval"); + + b.Property("SavedScriptId") + .HasColumnType("uuid"); + + b.Property("ScheduleId") + .HasColumnType("integer"); + + b.Property("ScriptInput") + .IsRequired() + .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("SavedScriptId"); + + b.HasIndex("ScheduleId"); + + b.HasIndex("ScriptRunId"); + + b.ToTable("ScriptResults"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.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") + .IsRequired() + .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.HasKey("Id"); + + b.HasIndex("OrganizationID"); + + b.HasIndex("SavedScriptId"); + + b.HasIndex("ScheduleId"); + + b.ToTable("ScriptRuns"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.ScriptSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Interval") + .HasColumnType("integer"); + + b.Property("LastRun") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NextRun") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationID") + .IsRequired() + .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.Entities.SharedFile", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("ContentType") + .HasColumnType("text"); + + b.Property("FileContents") + .IsRequired() + .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.DeviceGroup", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("OrganizationID") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("OrganizationID"); + + b.ToTable("DeviceGroups"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.RemotelyUser", b => + { + b.HasBaseType("Microsoft.AspNetCore.Identity.IdentityUser"); + + b.Property("IsAdministrator") + .HasColumnType("boolean"); + + b.Property("IsServerAdmin") + .HasColumnType("boolean"); + + b.Property("OrganizationID") + .IsRequired() + .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.Entities.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.Entities.ScriptSchedule", null) + .WithMany() + .HasForeignKey("ScriptSchedulesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("DeviceScriptRun", b => + { + b.HasOne("Remotely.Shared.Entities.Device", null) + .WithMany() + .HasForeignKey("DevicesID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.ScriptRun", null) + .WithMany() + .HasForeignKey("ScriptRunsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("DeviceScriptSchedule", b => + { + b.HasOne("Remotely.Shared.Entities.Device", null) + .WithMany() + .HasForeignKey("DevicesID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.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.Entities.Alert", b => + { + b.HasOne("Remotely.Shared.Entities.Device", "Device") + .WithMany("Alerts") + .HasForeignKey("DeviceID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("Alerts") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.RemotelyUser", "User") + .WithMany("Alerts") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.ApiToken", b => + { + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("ApiTokens") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.BrandingInfo", b => + { + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithOne("BrandingInfo") + .HasForeignKey("Remotely.Shared.Entities.BrandingInfo", "OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.Device", b => + { + b.HasOne("Remotely.Shared.Models.DeviceGroup", "DeviceGroup") + .WithMany("Devices") + .HasForeignKey("DeviceGroupID"); + + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("Devices") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DeviceGroup"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.InviteLink", b => + { + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("InviteLinks") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.SavedScript", b => + { + b.HasOne("Remotely.Shared.Entities.RemotelyUser", "Creator") + .WithMany("SavedScripts") + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("SavedScripts") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.ScriptResult", b => + { + b.HasOne("Remotely.Shared.Entities.Device", "Device") + .WithMany("ScriptResults") + .HasForeignKey("DeviceID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("ScriptResults") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.SavedScript", "SavedScript") + .WithMany("ScriptResults") + .HasForeignKey("SavedScriptId"); + + b.HasOne("Remotely.Shared.Entities.ScriptSchedule", "Schedule") + .WithMany() + .HasForeignKey("ScheduleId"); + + b.HasOne("Remotely.Shared.Entities.ScriptRun", "ScriptRun") + .WithMany("Results") + .HasForeignKey("ScriptRunId"); + + b.Navigation("Device"); + + b.Navigation("Organization"); + + b.Navigation("SavedScript"); + + b.Navigation("Schedule"); + + b.Navigation("ScriptRun"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.ScriptRun", b => + { + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("ScriptRuns") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.SavedScript", "SavedScript") + .WithMany("ScriptRuns") + .HasForeignKey("SavedScriptId"); + + b.HasOne("Remotely.Shared.Entities.ScriptSchedule", "Schedule") + .WithMany("ScriptRuns") + .HasForeignKey("ScheduleId"); + + b.Navigation("Organization"); + + b.Navigation("SavedScript"); + + b.Navigation("Schedule"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.ScriptSchedule", b => + { + b.HasOne("Remotely.Shared.Entities.RemotelyUser", "Creator") + .WithMany("ScriptSchedules") + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("ScriptSchedules") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.SharedFile", b => + { + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("SharedFiles") + .HasForeignKey("OrganizationID"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.DeviceGroup", b => + { + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("DeviceGroups") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.RemotelyUser", b => + { + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("RemotelyUsers") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.Device", b => + { + b.Navigation("Alerts"); + + b.Navigation("ScriptResults"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.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.Entities.SavedScript", b => + { + b.Navigation("ScriptResults"); + + b.Navigation("ScriptRuns"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.ScriptRun", b => + { + b.Navigation("Results"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.ScriptSchedule", b => + { + b.Navigation("ScriptRuns"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.DeviceGroup", b => + { + b.Navigation("Devices"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.RemotelyUser", b => + { + b.Navigation("Alerts"); + + b.Navigation("SavedScripts"); + + b.Navigation("ScriptSchedules"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/PostgreSql/20230726225818_Enable_NullableReferences.cs b/Server/Migrations/PostgreSql/20230726225818_Enable_NullableReferences.cs new file mode 100644 index 000000000..1f7175a92 --- /dev/null +++ b/Server/Migrations/PostgreSql/20230726225818_Enable_NullableReferences.cs @@ -0,0 +1,896 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Remotely.Server.Migrations.PostgreSql; + +/// +public partial class Enable_NullableReferences : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // This relationship wasn't enforced previously. + migrationBuilder.Sql("delete from ScriptResults where DeviceID is null;"); + + migrationBuilder.DropForeignKey( + name: "FK_Alerts_Devices_DeviceID", + table: "Alerts"); + + migrationBuilder.DropForeignKey( + name: "FK_Alerts_Organizations_OrganizationID", + table: "Alerts"); + + migrationBuilder.DropForeignKey( + name: "FK_Alerts_RemotelyUsers_UserID", + table: "Alerts"); + + migrationBuilder.DropForeignKey( + name: "FK_ApiTokens_Organizations_OrganizationID", + table: "ApiTokens"); + + migrationBuilder.DropForeignKey( + name: "FK_BrandingInfos_Organizations_OrganizationId", + table: "BrandingInfos"); + + migrationBuilder.DropForeignKey( + name: "FK_DeviceGroups_Organizations_OrganizationID", + table: "DeviceGroups"); + + migrationBuilder.DropForeignKey( + name: "FK_Devices_Organizations_OrganizationID", + table: "Devices"); + + migrationBuilder.DropForeignKey( + name: "FK_InviteLinks_Organizations_OrganizationID", + table: "InviteLinks"); + + migrationBuilder.DropForeignKey( + name: "FK_RemotelyUsers_Organizations_OrganizationID", + table: "RemotelyUsers"); + + migrationBuilder.DropForeignKey( + name: "FK_SavedScripts_Organizations_OrganizationID", + table: "SavedScripts"); + + migrationBuilder.DropForeignKey( + name: "FK_SavedScripts_RemotelyUsers_CreatorId", + table: "SavedScripts"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptResults_Devices_DeviceID", + table: "ScriptResults"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptResults_Organizations_OrganizationID", + table: "ScriptResults"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptRuns_Organizations_OrganizationID", + table: "ScriptRuns"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptRuns_ScriptSchedules_ScriptScheduleId", + table: "ScriptRuns"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptSchedules_Organizations_OrganizationID", + table: "ScriptSchedules"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptSchedules_RemotelyUsers_CreatorId", + table: "ScriptSchedules"); + + migrationBuilder.DropTable( + name: "DeviceScriptRun1"); + + migrationBuilder.DropIndex( + name: "IX_ScriptRuns_ScriptScheduleId", + table: "ScriptRuns"); + + migrationBuilder.DropColumn( + name: "ScriptScheduleId", + table: "ScriptRuns"); + + migrationBuilder.AlterColumn( + name: "FileContents", + table: "SharedFiles", + type: "bytea", + nullable: false, + defaultValue: new byte[0], + oldClrType: typeof(byte[]), + oldType: "bytea", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "ScriptSchedules", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Name", + table: "ScriptSchedules", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatorId", + table: "ScriptSchedules", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "ScriptRuns", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "ScriptInput", + table: "ScriptResults", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "ScriptResults", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeviceID", + table: "ScriptResults", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "SavedScripts", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatorId", + table: "SavedScripts", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationName", + table: "Organizations", + type: "character varying(25)", + maxLength: 25, + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "character varying(25)", + oldMaxLength: 25, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "InviteLinks", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "Devices", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "MacAddresses", + table: "Devices", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "DeviceGroups", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Name", + table: "DeviceGroups", + type: "character varying(200)", + maxLength: 200, + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "character varying(200)", + oldMaxLength: 200, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationId", + table: "BrandingInfos", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "ApiTokens", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UserID", + table: "Alerts", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "Alerts", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeviceID", + table: "Alerts", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.CreateIndex( + name: "IX_ScriptRuns_SavedScriptId", + table: "ScriptRuns", + column: "SavedScriptId"); + + migrationBuilder.CreateIndex( + name: "IX_ScriptRuns_ScheduleId", + table: "ScriptRuns", + column: "ScheduleId"); + + migrationBuilder.CreateIndex( + name: "IX_ScriptResults_SavedScriptId", + table: "ScriptResults", + column: "SavedScriptId"); + + migrationBuilder.AddForeignKey( + name: "FK_Alerts_Devices_DeviceID", + table: "Alerts", + column: "DeviceID", + principalTable: "Devices", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Alerts_Organizations_OrganizationID", + table: "Alerts", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Alerts_RemotelyUsers_UserID", + table: "Alerts", + column: "UserID", + principalTable: "RemotelyUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_ApiTokens_Organizations_OrganizationID", + table: "ApiTokens", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_BrandingInfos_Organizations_OrganizationId", + table: "BrandingInfos", + column: "OrganizationId", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_DeviceGroups_Organizations_OrganizationID", + table: "DeviceGroups", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Devices_Organizations_OrganizationID", + table: "Devices", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_InviteLinks_Organizations_OrganizationID", + table: "InviteLinks", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_RemotelyUsers_Organizations_OrganizationID", + table: "RemotelyUsers", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_SavedScripts_Organizations_OrganizationID", + table: "SavedScripts", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_SavedScripts_RemotelyUsers_CreatorId", + table: "SavedScripts", + column: "CreatorId", + principalTable: "RemotelyUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptResults_Devices_DeviceID", + table: "ScriptResults", + column: "DeviceID", + principalTable: "Devices", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptResults_Organizations_OrganizationID", + table: "ScriptResults", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptResults_SavedScripts_SavedScriptId", + table: "ScriptResults", + column: "SavedScriptId", + principalTable: "SavedScripts", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptRuns_Organizations_OrganizationID", + table: "ScriptRuns", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptRuns_SavedScripts_SavedScriptId", + table: "ScriptRuns", + column: "SavedScriptId", + principalTable: "SavedScripts", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptRuns_ScriptSchedules_ScheduleId", + table: "ScriptRuns", + column: "ScheduleId", + principalTable: "ScriptSchedules", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptSchedules_Organizations_OrganizationID", + table: "ScriptSchedules", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptSchedules_RemotelyUsers_CreatorId", + table: "ScriptSchedules", + column: "CreatorId", + principalTable: "RemotelyUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Alerts_Devices_DeviceID", + table: "Alerts"); + + migrationBuilder.DropForeignKey( + name: "FK_Alerts_Organizations_OrganizationID", + table: "Alerts"); + + migrationBuilder.DropForeignKey( + name: "FK_Alerts_RemotelyUsers_UserID", + table: "Alerts"); + + migrationBuilder.DropForeignKey( + name: "FK_ApiTokens_Organizations_OrganizationID", + table: "ApiTokens"); + + migrationBuilder.DropForeignKey( + name: "FK_BrandingInfos_Organizations_OrganizationId", + table: "BrandingInfos"); + + migrationBuilder.DropForeignKey( + name: "FK_DeviceGroups_Organizations_OrganizationID", + table: "DeviceGroups"); + + migrationBuilder.DropForeignKey( + name: "FK_Devices_Organizations_OrganizationID", + table: "Devices"); + + migrationBuilder.DropForeignKey( + name: "FK_InviteLinks_Organizations_OrganizationID", + table: "InviteLinks"); + + migrationBuilder.DropForeignKey( + name: "FK_RemotelyUsers_Organizations_OrganizationID", + table: "RemotelyUsers"); + + migrationBuilder.DropForeignKey( + name: "FK_SavedScripts_Organizations_OrganizationID", + table: "SavedScripts"); + + migrationBuilder.DropForeignKey( + name: "FK_SavedScripts_RemotelyUsers_CreatorId", + table: "SavedScripts"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptResults_Devices_DeviceID", + table: "ScriptResults"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptResults_Organizations_OrganizationID", + table: "ScriptResults"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptResults_SavedScripts_SavedScriptId", + table: "ScriptResults"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptRuns_Organizations_OrganizationID", + table: "ScriptRuns"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptRuns_SavedScripts_SavedScriptId", + table: "ScriptRuns"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptRuns_ScriptSchedules_ScheduleId", + table: "ScriptRuns"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptSchedules_Organizations_OrganizationID", + table: "ScriptSchedules"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptSchedules_RemotelyUsers_CreatorId", + table: "ScriptSchedules"); + + migrationBuilder.DropIndex( + name: "IX_ScriptRuns_SavedScriptId", + table: "ScriptRuns"); + + migrationBuilder.DropIndex( + name: "IX_ScriptRuns_ScheduleId", + table: "ScriptRuns"); + + migrationBuilder.DropIndex( + name: "IX_ScriptResults_SavedScriptId", + table: "ScriptResults"); + + migrationBuilder.AlterColumn( + name: "FileContents", + table: "SharedFiles", + type: "bytea", + nullable: true, + oldClrType: typeof(byte[]), + oldType: "bytea"); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "ScriptSchedules", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "ScriptSchedules", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "CreatorId", + table: "ScriptSchedules", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "ScriptRuns", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AddColumn( + name: "ScriptScheduleId", + table: "ScriptRuns", + type: "integer", + nullable: true); + + migrationBuilder.AlterColumn( + name: "ScriptInput", + table: "ScriptResults", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "ScriptResults", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "DeviceID", + table: "ScriptResults", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "SavedScripts", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "CreatorId", + table: "SavedScripts", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "OrganizationName", + table: "Organizations", + type: "character varying(25)", + maxLength: 25, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(25)", + oldMaxLength: 25); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "InviteLinks", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "Devices", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "MacAddresses", + table: "Devices", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "DeviceGroups", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "DeviceGroups", + type: "character varying(200)", + maxLength: 200, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(200)", + oldMaxLength: 200); + + migrationBuilder.AlterColumn( + name: "OrganizationId", + table: "BrandingInfos", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "ApiTokens", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "UserID", + table: "Alerts", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "Alerts", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "DeviceID", + table: "Alerts", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.CreateTable( + name: "DeviceScriptRun1", + columns: table => new + { + DevicesCompletedID = table.Column(type: "text", nullable: false), + ScriptRunsCompletedId = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_DeviceScriptRun1", x => new { x.DevicesCompletedID, x.ScriptRunsCompletedId }); + table.ForeignKey( + name: "FK_DeviceScriptRun1_Devices_DevicesCompletedID", + column: x => x.DevicesCompletedID, + principalTable: "Devices", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_DeviceScriptRun1_ScriptRuns_ScriptRunsCompletedId", + column: x => x.ScriptRunsCompletedId, + principalTable: "ScriptRuns", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ScriptRuns_ScriptScheduleId", + table: "ScriptRuns", + column: "ScriptScheduleId"); + + migrationBuilder.CreateIndex( + name: "IX_DeviceScriptRun1_ScriptRunsCompletedId", + table: "DeviceScriptRun1", + column: "ScriptRunsCompletedId"); + + migrationBuilder.AddForeignKey( + name: "FK_Alerts_Devices_DeviceID", + table: "Alerts", + column: "DeviceID", + principalTable: "Devices", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_Alerts_Organizations_OrganizationID", + table: "Alerts", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_Alerts_RemotelyUsers_UserID", + table: "Alerts", + column: "UserID", + principalTable: "RemotelyUsers", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_ApiTokens_Organizations_OrganizationID", + table: "ApiTokens", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_BrandingInfos_Organizations_OrganizationId", + table: "BrandingInfos", + column: "OrganizationId", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_DeviceGroups_Organizations_OrganizationID", + table: "DeviceGroups", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_Devices_Organizations_OrganizationID", + table: "Devices", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_InviteLinks_Organizations_OrganizationID", + table: "InviteLinks", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_RemotelyUsers_Organizations_OrganizationID", + table: "RemotelyUsers", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_SavedScripts_Organizations_OrganizationID", + table: "SavedScripts", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_SavedScripts_RemotelyUsers_CreatorId", + table: "SavedScripts", + column: "CreatorId", + principalTable: "RemotelyUsers", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptResults_Devices_DeviceID", + table: "ScriptResults", + column: "DeviceID", + principalTable: "Devices", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptResults_Organizations_OrganizationID", + table: "ScriptResults", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptRuns_Organizations_OrganizationID", + table: "ScriptRuns", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptRuns_ScriptSchedules_ScriptScheduleId", + table: "ScriptRuns", + column: "ScriptScheduleId", + principalTable: "ScriptSchedules", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptSchedules_Organizations_OrganizationID", + table: "ScriptSchedules", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptSchedules_RemotelyUsers_CreatorId", + table: "ScriptSchedules", + column: "CreatorId", + principalTable: "RemotelyUsers", + principalColumn: "Id"); + } +} diff --git a/Server/Migrations/PostgreSql/PostgreSqlDbContextModelSnapshot.cs b/Server/Migrations/PostgreSql/PostgreSqlDbContextModelSnapshot.cs index fd996a8cf..7da292b08 100644 --- a/Server/Migrations/PostgreSql/PostgreSqlDbContextModelSnapshot.cs +++ b/Server/Migrations/PostgreSql/PostgreSqlDbContextModelSnapshot.cs @@ -67,21 +67,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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") @@ -301,7 +286,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AspNetUserTokens", (string)null); }); - modelBuilder.Entity("Remotely.Shared.Models.Alert", b => + modelBuilder.Entity("Remotely.Shared.Entities.Alert", b => { b.Property("ID") .ValueGeneratedOnAdd() @@ -314,15 +299,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text"); b.Property("DeviceID") + .IsRequired() .HasColumnType("text"); b.Property("Message") .HasColumnType("text"); b.Property("OrganizationID") + .IsRequired() .HasColumnType("text"); b.Property("UserID") + .IsRequired() .HasColumnType("text"); b.HasKey("ID"); @@ -336,7 +324,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Alerts"); }); - modelBuilder.Entity("Remotely.Shared.Models.ApiToken", b => + modelBuilder.Entity("Remotely.Shared.Entities.ApiToken", b => { b.Property("ID") .ValueGeneratedOnAdd() @@ -350,6 +338,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(200)"); b.Property("OrganizationID") + .IsRequired() .HasColumnType("text"); b.Property("Secret") @@ -362,7 +351,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ApiTokens"); }); - modelBuilder.Entity("Remotely.Shared.Models.BrandingInfo", b => + modelBuilder.Entity("Remotely.Shared.Entities.BrandingInfo", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -382,6 +371,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("bytea"); b.Property("OrganizationId") + .IsRequired() .HasColumnType("text"); b.Property("Product") @@ -415,7 +405,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("BrandingInfos"); }); - modelBuilder.Entity("Remotely.Shared.Models.Device", b => + modelBuilder.Entity("Remotely.Shared.Entities.Device", b => { b.Property("ID") .HasColumnType("text"); @@ -452,6 +442,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone"); b.Property("MacAddresses") + .IsRequired() .HasColumnType("text"); b.Property("Notes") @@ -465,6 +456,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text"); b.Property("OrganizationID") + .IsRequired() .HasColumnType("text"); b.Property("Platform") @@ -506,27 +498,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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 => + modelBuilder.Entity("Remotely.Shared.Entities.InviteLink", b => { b.Property("ID") .ValueGeneratedOnAdd() @@ -542,6 +514,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("boolean"); b.Property("OrganizationID") + .IsRequired() .HasColumnType("text"); b.Property("ResetUrl") @@ -554,7 +527,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("InviteLinks"); }); - modelBuilder.Entity("Remotely.Shared.Models.Organization", b => + modelBuilder.Entity("Remotely.Shared.Entities.Organization", b => { b.Property("ID") .ValueGeneratedOnAdd() @@ -564,6 +537,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("boolean"); b.Property("OrganizationName") + .IsRequired() .HasMaxLength(25) .HasColumnType("character varying(25)"); @@ -572,7 +546,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Organizations"); }); - modelBuilder.Entity("Remotely.Shared.Models.SavedScript", b => + modelBuilder.Entity("Remotely.Shared.Entities.SavedScript", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -583,6 +557,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text"); b.Property("CreatorId") + .IsRequired() .HasColumnType("text"); b.Property("FolderPath") @@ -604,6 +579,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(100)"); b.Property("OrganizationID") + .IsRequired() .HasColumnType("text"); b.Property("SendEmailOnError") @@ -624,13 +600,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("SavedScripts"); }); - modelBuilder.Entity("Remotely.Shared.Models.ScriptResult", b => + modelBuilder.Entity("Remotely.Shared.Entities.ScriptResult", b => { b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("text"); b.Property("DeviceID") + .IsRequired() .HasColumnType("text"); b.Property("ErrorOutput") @@ -643,6 +620,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer"); b.Property("OrganizationID") + .IsRequired() .HasColumnType("text"); b.Property("RunTime") @@ -655,6 +633,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer"); b.Property("ScriptInput") + .IsRequired() .HasColumnType("text"); b.Property("ScriptRunId") @@ -681,6 +660,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("OrganizationID"); + b.HasIndex("SavedScriptId"); + b.HasIndex("ScheduleId"); b.HasIndex("ScriptRunId"); @@ -688,7 +669,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ScriptResults"); }); - modelBuilder.Entity("Remotely.Shared.Models.ScriptRun", b => + modelBuilder.Entity("Remotely.Shared.Entities.ScriptRun", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -703,6 +684,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("integer"); b.Property("OrganizationID") + .IsRequired() .HasColumnType("text"); b.Property("RunAt") @@ -717,19 +699,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ScheduleId") .HasColumnType("integer"); - b.Property("ScriptScheduleId") - .HasColumnType("integer"); - b.HasKey("Id"); b.HasIndex("OrganizationID"); - b.HasIndex("ScriptScheduleId"); + b.HasIndex("SavedScriptId"); + + b.HasIndex("ScheduleId"); b.ToTable("ScriptRuns"); }); - modelBuilder.Entity("Remotely.Shared.Models.ScriptSchedule", b => + modelBuilder.Entity("Remotely.Shared.Entities.ScriptSchedule", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -741,6 +722,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone"); b.Property("CreatorId") + .IsRequired() .HasColumnType("text"); b.Property("Interval") @@ -750,12 +732,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone"); b.Property("Name") + .IsRequired() .HasColumnType("text"); b.Property("NextRun") .HasColumnType("timestamp with time zone"); b.Property("OrganizationID") + .IsRequired() .HasColumnType("text"); b.Property("RunOnNextConnect") @@ -776,7 +760,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ScriptSchedules"); }); - modelBuilder.Entity("Remotely.Shared.Models.SharedFile", b => + modelBuilder.Entity("Remotely.Shared.Entities.SharedFile", b => { b.Property("ID") .ValueGeneratedOnAdd() @@ -786,6 +770,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text"); b.Property("FileContents") + .IsRequired() .HasColumnType("bytea"); b.Property("FileName") @@ -804,7 +789,29 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("SharedFiles"); }); - modelBuilder.Entity("Remotely.Shared.Models.RemotelyUser", b => + modelBuilder.Entity("Remotely.Shared.Models.DeviceGroup", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("OrganizationID") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ID"); + + b.HasIndex("OrganizationID"); + + b.ToTable("DeviceGroups"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.RemotelyUser", b => { b.HasBaseType("Microsoft.AspNetCore.Identity.IdentityUser"); @@ -815,6 +822,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("boolean"); b.Property("OrganizationID") + .IsRequired() .HasColumnType("text"); b.Property("TempPassword") @@ -838,7 +846,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Remotely.Shared.Models.RemotelyUser", null) + b.HasOne("Remotely.Shared.Entities.RemotelyUser", null) .WithMany() .HasForeignKey("UsersId") .OnDelete(DeleteBehavior.Cascade) @@ -853,7 +861,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Remotely.Shared.Models.ScriptSchedule", null) + b.HasOne("Remotely.Shared.Entities.ScriptSchedule", null) .WithMany() .HasForeignKey("ScriptSchedulesId") .OnDelete(DeleteBehavior.Cascade) @@ -862,43 +870,28 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("DeviceScriptRun", b => { - b.HasOne("Remotely.Shared.Models.Device", null) + b.HasOne("Remotely.Shared.Entities.Device", null) .WithMany() .HasForeignKey("DevicesID") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Remotely.Shared.Models.ScriptRun", null) + b.HasOne("Remotely.Shared.Entities.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) + b.HasOne("Remotely.Shared.Entities.Device", null) .WithMany() .HasForeignKey("DevicesID") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Remotely.Shared.Models.ScriptSchedule", null) + b.HasOne("Remotely.Shared.Entities.ScriptSchedule", null) .WithMany() .HasForeignKey("ScriptSchedulesId") .OnDelete(DeleteBehavior.Cascade) @@ -956,19 +949,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); - modelBuilder.Entity("Remotely.Shared.Models.Alert", b => + modelBuilder.Entity("Remotely.Shared.Entities.Alert", b => { - b.HasOne("Remotely.Shared.Models.Device", "Device") + b.HasOne("Remotely.Shared.Entities.Device", "Device") .WithMany("Alerts") - .HasForeignKey("DeviceID"); + .HasForeignKey("DeviceID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithMany("Alerts") - .HasForeignKey("OrganizationID"); + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Remotely.Shared.Models.RemotelyUser", "User") + b.HasOne("Remotely.Shared.Entities.RemotelyUser", "User") .WithMany("Alerts") - .HasForeignKey("UserID"); + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); b.Navigation("Device"); @@ -977,87 +976,98 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); - modelBuilder.Entity("Remotely.Shared.Models.ApiToken", b => + modelBuilder.Entity("Remotely.Shared.Entities.ApiToken", b => { - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithMany("ApiTokens") - .HasForeignKey("OrganizationID"); + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); b.Navigation("Organization"); }); - modelBuilder.Entity("Remotely.Shared.Models.BrandingInfo", b => + modelBuilder.Entity("Remotely.Shared.Entities.BrandingInfo", b => { - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithOne("BrandingInfo") - .HasForeignKey("Remotely.Shared.Models.BrandingInfo", "OrganizationId"); + .HasForeignKey("Remotely.Shared.Entities.BrandingInfo", "OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); b.Navigation("Organization"); }); - modelBuilder.Entity("Remotely.Shared.Models.Device", b => + modelBuilder.Entity("Remotely.Shared.Entities.Device", b => { b.HasOne("Remotely.Shared.Models.DeviceGroup", "DeviceGroup") .WithMany("Devices") .HasForeignKey("DeviceGroupID"); - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithMany("Devices") - .HasForeignKey("OrganizationID"); + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); 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 => + modelBuilder.Entity("Remotely.Shared.Entities.InviteLink", b => { - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithMany("InviteLinks") - .HasForeignKey("OrganizationID"); + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); b.Navigation("Organization"); }); - modelBuilder.Entity("Remotely.Shared.Models.SavedScript", b => + modelBuilder.Entity("Remotely.Shared.Entities.SavedScript", b => { - b.HasOne("Remotely.Shared.Models.RemotelyUser", "Creator") + b.HasOne("Remotely.Shared.Entities.RemotelyUser", "Creator") .WithMany("SavedScripts") - .HasForeignKey("CreatorId"); + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithMany("SavedScripts") - .HasForeignKey("OrganizationID"); + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); b.Navigation("Creator"); b.Navigation("Organization"); }); - modelBuilder.Entity("Remotely.Shared.Models.ScriptResult", b => + modelBuilder.Entity("Remotely.Shared.Entities.ScriptResult", b => { - b.HasOne("Remotely.Shared.Models.Device", "Device") + b.HasOne("Remotely.Shared.Entities.Device", "Device") .WithMany("ScriptResults") - .HasForeignKey("DeviceID"); + .HasForeignKey("DeviceID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithMany("ScriptResults") - .HasForeignKey("OrganizationID"); + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.SavedScript", "SavedScript") + .WithMany("ScriptResults") + .HasForeignKey("SavedScriptId"); - b.HasOne("Remotely.Shared.Models.ScriptSchedule", "Schedule") + b.HasOne("Remotely.Shared.Entities.ScriptSchedule", "Schedule") .WithMany() .HasForeignKey("ScheduleId"); - b.HasOne("Remotely.Shared.Models.ScriptRun", null) + b.HasOne("Remotely.Shared.Entities.ScriptRun", "ScriptRun") .WithMany("Results") .HasForeignKey("ScriptRunId"); @@ -1065,68 +1075,94 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Organization"); + b.Navigation("SavedScript"); + b.Navigation("Schedule"); + + b.Navigation("ScriptRun"); }); - modelBuilder.Entity("Remotely.Shared.Models.ScriptRun", b => + modelBuilder.Entity("Remotely.Shared.Entities.ScriptRun", b => { - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithMany("ScriptRuns") - .HasForeignKey("OrganizationID"); + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Remotely.Shared.Models.ScriptSchedule", null) + b.HasOne("Remotely.Shared.Entities.SavedScript", "SavedScript") .WithMany("ScriptRuns") - .HasForeignKey("ScriptScheduleId"); + .HasForeignKey("SavedScriptId"); + + b.HasOne("Remotely.Shared.Entities.ScriptSchedule", "Schedule") + .WithMany("ScriptRuns") + .HasForeignKey("ScheduleId"); b.Navigation("Organization"); + + b.Navigation("SavedScript"); + + b.Navigation("Schedule"); }); - modelBuilder.Entity("Remotely.Shared.Models.ScriptSchedule", b => + modelBuilder.Entity("Remotely.Shared.Entities.ScriptSchedule", b => { - b.HasOne("Remotely.Shared.Models.RemotelyUser", "Creator") + b.HasOne("Remotely.Shared.Entities.RemotelyUser", "Creator") .WithMany("ScriptSchedules") - .HasForeignKey("CreatorId"); + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithMany("ScriptSchedules") - .HasForeignKey("OrganizationID"); + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); b.Navigation("Creator"); b.Navigation("Organization"); }); - modelBuilder.Entity("Remotely.Shared.Models.SharedFile", b => + modelBuilder.Entity("Remotely.Shared.Entities.SharedFile", b => { - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithMany("SharedFiles") .HasForeignKey("OrganizationID"); b.Navigation("Organization"); }); - modelBuilder.Entity("Remotely.Shared.Models.RemotelyUser", b => + modelBuilder.Entity("Remotely.Shared.Models.DeviceGroup", b => { - b.HasOne("Remotely.Shared.Models.Organization", "Organization") - .WithMany("RemotelyUsers") - .HasForeignKey("OrganizationID"); + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("DeviceGroups") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); b.Navigation("Organization"); }); - modelBuilder.Entity("Remotely.Shared.Models.Device", b => + modelBuilder.Entity("Remotely.Shared.Entities.RemotelyUser", b => { - b.Navigation("Alerts"); + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("RemotelyUsers") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.Navigation("ScriptResults"); + b.Navigation("Organization"); }); - modelBuilder.Entity("Remotely.Shared.Models.DeviceGroup", b => + modelBuilder.Entity("Remotely.Shared.Entities.Device", b => { - b.Navigation("Devices"); + b.Navigation("Alerts"); + + b.Navigation("ScriptResults"); }); - modelBuilder.Entity("Remotely.Shared.Models.Organization", b => + modelBuilder.Entity("Remotely.Shared.Entities.Organization", b => { b.Navigation("Alerts"); @@ -1153,17 +1189,29 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("SharedFiles"); }); - modelBuilder.Entity("Remotely.Shared.Models.ScriptRun", b => + modelBuilder.Entity("Remotely.Shared.Entities.SavedScript", b => + { + b.Navigation("ScriptResults"); + + b.Navigation("ScriptRuns"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.ScriptRun", b => { b.Navigation("Results"); }); - modelBuilder.Entity("Remotely.Shared.Models.ScriptSchedule", b => + modelBuilder.Entity("Remotely.Shared.Entities.ScriptSchedule", b => { b.Navigation("ScriptRuns"); }); - modelBuilder.Entity("Remotely.Shared.Models.RemotelyUser", b => + modelBuilder.Entity("Remotely.Shared.Models.DeviceGroup", b => + { + b.Navigation("Devices"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.RemotelyUser", b => { b.Navigation("Alerts"); diff --git a/Server/Migrations/SqlServer/20230726225809_Enable_NullableReferences.Designer.cs b/Server/Migrations/SqlServer/20230726225809_Enable_NullableReferences.Designer.cs new file mode 100644 index 000000000..325b75787 --- /dev/null +++ b/Server/Migrations/SqlServer/20230726225809_Enable_NullableReferences.Designer.cs @@ -0,0 +1,1230 @@ +// +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("20230726225809_Enable_NullableReferences")] + partial class Enable_NullableReferences + { + /// + 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("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.Entities.Alert", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("CreatedOn") + .HasColumnType("datetimeoffset"); + + b.Property("Details") + .HasColumnType("nvarchar(max)"); + + b.Property("DeviceID") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Message") + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizationID") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("UserID") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("ID"); + + b.HasIndex("DeviceID"); + + b.HasIndex("OrganizationID"); + + b.HasIndex("UserID"); + + b.ToTable("Alerts"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.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") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Secret") + .HasColumnType("nvarchar(max)"); + + b.HasKey("ID"); + + b.HasIndex("OrganizationID"); + + b.ToTable("ApiTokens"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.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") + .IsRequired() + .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(); + + b.ToTable("BrandingInfos"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.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") + .IsRequired() + .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") + .IsRequired() + .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.Entities.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") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("ResetUrl") + .HasColumnType("nvarchar(max)"); + + b.HasKey("ID"); + + b.HasIndex("OrganizationID"); + + b.ToTable("InviteLinks"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.Organization", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("IsDefaultOrganization") + .HasColumnType("bit"); + + b.Property("OrganizationName") + .IsRequired() + .HasMaxLength(25) + .HasColumnType("nvarchar(25)"); + + b.HasKey("ID"); + + b.ToTable("Organizations"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.SavedScript", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatorId") + .IsRequired() + .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") + .IsRequired() + .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.Entities.ScriptResult", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("DeviceID") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("ErrorOutput") + .HasColumnType("nvarchar(max)"); + + b.Property("HadErrors") + .HasColumnType("bit"); + + b.Property("InputType") + .HasColumnType("int"); + + b.Property("OrganizationID") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("RunTime") + .HasColumnType("time"); + + b.Property("SavedScriptId") + .HasColumnType("uniqueidentifier"); + + b.Property("ScheduleId") + .HasColumnType("int"); + + b.Property("ScriptInput") + .IsRequired() + .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("SavedScriptId"); + + b.HasIndex("ScheduleId"); + + b.HasIndex("ScriptRunId"); + + b.ToTable("ScriptResults"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.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") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("RunAt") + .HasColumnType("datetimeoffset"); + + b.Property("RunOnNextConnect") + .HasColumnType("bit"); + + b.Property("SavedScriptId") + .HasColumnType("uniqueidentifier"); + + b.Property("ScheduleId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationID"); + + b.HasIndex("SavedScriptId"); + + b.HasIndex("ScheduleId"); + + b.ToTable("ScriptRuns"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.ScriptSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatorId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Interval") + .HasColumnType("int"); + + b.Property("LastRun") + .HasColumnType("datetimeoffset"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("NextRun") + .HasColumnType("datetimeoffset"); + + b.Property("OrganizationID") + .IsRequired() + .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.Entities.SharedFile", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("ContentType") + .HasColumnType("nvarchar(max)"); + + b.Property("FileContents") + .IsRequired() + .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.DeviceGroup", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OrganizationID") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("ID"); + + b.HasIndex("OrganizationID"); + + b.ToTable("DeviceGroups"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.RemotelyUser", b => + { + b.HasBaseType("Microsoft.AspNetCore.Identity.IdentityUser"); + + b.Property("IsAdministrator") + .HasColumnType("bit"); + + b.Property("IsServerAdmin") + .HasColumnType("bit"); + + b.Property("OrganizationID") + .IsRequired() + .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.Entities.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.Entities.ScriptSchedule", null) + .WithMany() + .HasForeignKey("ScriptSchedulesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("DeviceScriptRun", b => + { + b.HasOne("Remotely.Shared.Entities.Device", null) + .WithMany() + .HasForeignKey("DevicesID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.ScriptRun", null) + .WithMany() + .HasForeignKey("ScriptRunsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("DeviceScriptSchedule", b => + { + b.HasOne("Remotely.Shared.Entities.Device", null) + .WithMany() + .HasForeignKey("DevicesID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.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.Entities.Alert", b => + { + b.HasOne("Remotely.Shared.Entities.Device", "Device") + .WithMany("Alerts") + .HasForeignKey("DeviceID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("Alerts") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.RemotelyUser", "User") + .WithMany("Alerts") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.ApiToken", b => + { + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("ApiTokens") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.BrandingInfo", b => + { + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithOne("BrandingInfo") + .HasForeignKey("Remotely.Shared.Entities.BrandingInfo", "OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.Device", b => + { + b.HasOne("Remotely.Shared.Models.DeviceGroup", "DeviceGroup") + .WithMany("Devices") + .HasForeignKey("DeviceGroupID"); + + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("Devices") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DeviceGroup"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.InviteLink", b => + { + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("InviteLinks") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.SavedScript", b => + { + b.HasOne("Remotely.Shared.Entities.RemotelyUser", "Creator") + .WithMany("SavedScripts") + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("SavedScripts") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.ScriptResult", b => + { + b.HasOne("Remotely.Shared.Entities.Device", "Device") + .WithMany("ScriptResults") + .HasForeignKey("DeviceID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("ScriptResults") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.SavedScript", "SavedScript") + .WithMany("ScriptResults") + .HasForeignKey("SavedScriptId"); + + b.HasOne("Remotely.Shared.Entities.ScriptSchedule", "Schedule") + .WithMany() + .HasForeignKey("ScheduleId"); + + b.HasOne("Remotely.Shared.Entities.ScriptRun", "ScriptRun") + .WithMany("Results") + .HasForeignKey("ScriptRunId"); + + b.Navigation("Device"); + + b.Navigation("Organization"); + + b.Navigation("SavedScript"); + + b.Navigation("Schedule"); + + b.Navigation("ScriptRun"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.ScriptRun", b => + { + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("ScriptRuns") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.SavedScript", "SavedScript") + .WithMany("ScriptRuns") + .HasForeignKey("SavedScriptId"); + + b.HasOne("Remotely.Shared.Entities.ScriptSchedule", "Schedule") + .WithMany("ScriptRuns") + .HasForeignKey("ScheduleId"); + + b.Navigation("Organization"); + + b.Navigation("SavedScript"); + + b.Navigation("Schedule"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.ScriptSchedule", b => + { + b.HasOne("Remotely.Shared.Entities.RemotelyUser", "Creator") + .WithMany("ScriptSchedules") + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("ScriptSchedules") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.SharedFile", b => + { + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("SharedFiles") + .HasForeignKey("OrganizationID"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.DeviceGroup", b => + { + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("DeviceGroups") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.RemotelyUser", b => + { + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("RemotelyUsers") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.Device", b => + { + b.Navigation("Alerts"); + + b.Navigation("ScriptResults"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.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.Entities.SavedScript", b => + { + b.Navigation("ScriptResults"); + + b.Navigation("ScriptRuns"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.ScriptRun", b => + { + b.Navigation("Results"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.ScriptSchedule", b => + { + b.Navigation("ScriptRuns"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.DeviceGroup", b => + { + b.Navigation("Devices"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.RemotelyUser", b => + { + b.Navigation("Alerts"); + + b.Navigation("SavedScripts"); + + b.Navigation("ScriptSchedules"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/SqlServer/20230726225809_Enable_NullableReferences.cs b/Server/Migrations/SqlServer/20230726225809_Enable_NullableReferences.cs new file mode 100644 index 000000000..6adcd6556 --- /dev/null +++ b/Server/Migrations/SqlServer/20230726225809_Enable_NullableReferences.cs @@ -0,0 +1,917 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Remotely.Server.Migrations.SqlServer; + +/// +public partial class Enable_NullableReferences : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // This relationship wasn't enforced previously. + migrationBuilder.Sql("delete from ScriptResults where DeviceID is null;"); + + migrationBuilder.DropForeignKey( + name: "FK_Alerts_Devices_DeviceID", + table: "Alerts"); + + migrationBuilder.DropForeignKey( + name: "FK_Alerts_Organizations_OrganizationID", + table: "Alerts"); + + migrationBuilder.DropForeignKey( + name: "FK_Alerts_RemotelyUsers_UserID", + table: "Alerts"); + + migrationBuilder.DropForeignKey( + name: "FK_ApiTokens_Organizations_OrganizationID", + table: "ApiTokens"); + + migrationBuilder.DropForeignKey( + name: "FK_BrandingInfos_Organizations_OrganizationId", + table: "BrandingInfos"); + + migrationBuilder.DropForeignKey( + name: "FK_DeviceGroups_Organizations_OrganizationID", + table: "DeviceGroups"); + + migrationBuilder.DropForeignKey( + name: "FK_Devices_Organizations_OrganizationID", + table: "Devices"); + + migrationBuilder.DropForeignKey( + name: "FK_InviteLinks_Organizations_OrganizationID", + table: "InviteLinks"); + + migrationBuilder.DropForeignKey( + name: "FK_RemotelyUsers_Organizations_OrganizationID", + table: "RemotelyUsers"); + + migrationBuilder.DropForeignKey( + name: "FK_SavedScripts_Organizations_OrganizationID", + table: "SavedScripts"); + + migrationBuilder.DropForeignKey( + name: "FK_SavedScripts_RemotelyUsers_CreatorId", + table: "SavedScripts"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptResults_Devices_DeviceID", + table: "ScriptResults"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptResults_Organizations_OrganizationID", + table: "ScriptResults"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptRuns_Organizations_OrganizationID", + table: "ScriptRuns"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptRuns_ScriptSchedules_ScriptScheduleId", + table: "ScriptRuns"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptSchedules_Organizations_OrganizationID", + table: "ScriptSchedules"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptSchedules_RemotelyUsers_CreatorId", + table: "ScriptSchedules"); + + migrationBuilder.DropTable( + name: "DeviceScriptRun1"); + + migrationBuilder.DropIndex( + name: "IX_ScriptRuns_ScriptScheduleId", + table: "ScriptRuns"); + + migrationBuilder.DropIndex( + name: "IX_BrandingInfos_OrganizationId", + table: "BrandingInfos"); + + migrationBuilder.DropColumn( + name: "ScriptScheduleId", + table: "ScriptRuns"); + + migrationBuilder.AlterColumn( + name: "FileContents", + table: "SharedFiles", + type: "varbinary(max)", + nullable: false, + defaultValue: new byte[0], + oldClrType: typeof(byte[]), + oldType: "varbinary(max)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "ScriptSchedules", + type: "nvarchar(450)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(450)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Name", + table: "ScriptSchedules", + type: "nvarchar(max)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(max)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatorId", + table: "ScriptSchedules", + type: "nvarchar(450)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(450)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "ScriptRuns", + type: "nvarchar(450)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(450)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "ScriptInput", + table: "ScriptResults", + type: "nvarchar(max)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(max)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "ScriptResults", + type: "nvarchar(450)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(450)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeviceID", + table: "ScriptResults", + type: "nvarchar(450)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(450)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "SavedScripts", + type: "nvarchar(450)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(450)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatorId", + table: "SavedScripts", + type: "nvarchar(450)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(450)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationName", + table: "Organizations", + type: "nvarchar(25)", + maxLength: 25, + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(25)", + oldMaxLength: 25, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "InviteLinks", + type: "nvarchar(450)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(450)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "Devices", + type: "nvarchar(450)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(450)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "MacAddresses", + table: "Devices", + type: "nvarchar(max)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(max)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "DeviceGroups", + type: "nvarchar(450)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(450)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Name", + table: "DeviceGroups", + type: "nvarchar(200)", + maxLength: 200, + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(200)", + oldMaxLength: 200, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationId", + table: "BrandingInfos", + type: "nvarchar(450)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(450)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "ApiTokens", + type: "nvarchar(450)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(450)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UserID", + table: "Alerts", + type: "nvarchar(450)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(450)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "Alerts", + type: "nvarchar(450)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(450)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeviceID", + table: "Alerts", + type: "nvarchar(450)", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "nvarchar(450)", + oldNullable: true); + + migrationBuilder.CreateIndex( + name: "IX_ScriptRuns_SavedScriptId", + table: "ScriptRuns", + column: "SavedScriptId"); + + migrationBuilder.CreateIndex( + name: "IX_ScriptRuns_ScheduleId", + table: "ScriptRuns", + column: "ScheduleId"); + + migrationBuilder.CreateIndex( + name: "IX_ScriptResults_SavedScriptId", + table: "ScriptResults", + column: "SavedScriptId"); + + migrationBuilder.CreateIndex( + name: "IX_BrandingInfos_OrganizationId", + table: "BrandingInfos", + column: "OrganizationId", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_Alerts_Devices_DeviceID", + table: "Alerts", + column: "DeviceID", + principalTable: "Devices", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Alerts_Organizations_OrganizationID", + table: "Alerts", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Alerts_RemotelyUsers_UserID", + table: "Alerts", + column: "UserID", + principalTable: "RemotelyUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_ApiTokens_Organizations_OrganizationID", + table: "ApiTokens", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_BrandingInfos_Organizations_OrganizationId", + table: "BrandingInfos", + column: "OrganizationId", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_DeviceGroups_Organizations_OrganizationID", + table: "DeviceGroups", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Devices_Organizations_OrganizationID", + table: "Devices", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_InviteLinks_Organizations_OrganizationID", + table: "InviteLinks", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_RemotelyUsers_Organizations_OrganizationID", + table: "RemotelyUsers", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_SavedScripts_Organizations_OrganizationID", + table: "SavedScripts", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_SavedScripts_RemotelyUsers_CreatorId", + table: "SavedScripts", + column: "CreatorId", + principalTable: "RemotelyUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptResults_Devices_DeviceID", + table: "ScriptResults", + column: "DeviceID", + principalTable: "Devices", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptResults_Organizations_OrganizationID", + table: "ScriptResults", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptResults_SavedScripts_SavedScriptId", + table: "ScriptResults", + column: "SavedScriptId", + principalTable: "SavedScripts", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptRuns_Organizations_OrganizationID", + table: "ScriptRuns", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptRuns_SavedScripts_SavedScriptId", + table: "ScriptRuns", + column: "SavedScriptId", + principalTable: "SavedScripts", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptRuns_ScriptSchedules_ScheduleId", + table: "ScriptRuns", + column: "ScheduleId", + principalTable: "ScriptSchedules", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptSchedules_Organizations_OrganizationID", + table: "ScriptSchedules", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptSchedules_RemotelyUsers_CreatorId", + table: "ScriptSchedules", + column: "CreatorId", + principalTable: "RemotelyUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Alerts_Devices_DeviceID", + table: "Alerts"); + + migrationBuilder.DropForeignKey( + name: "FK_Alerts_Organizations_OrganizationID", + table: "Alerts"); + + migrationBuilder.DropForeignKey( + name: "FK_Alerts_RemotelyUsers_UserID", + table: "Alerts"); + + migrationBuilder.DropForeignKey( + name: "FK_ApiTokens_Organizations_OrganizationID", + table: "ApiTokens"); + + migrationBuilder.DropForeignKey( + name: "FK_BrandingInfos_Organizations_OrganizationId", + table: "BrandingInfos"); + + migrationBuilder.DropForeignKey( + name: "FK_DeviceGroups_Organizations_OrganizationID", + table: "DeviceGroups"); + + migrationBuilder.DropForeignKey( + name: "FK_Devices_Organizations_OrganizationID", + table: "Devices"); + + migrationBuilder.DropForeignKey( + name: "FK_InviteLinks_Organizations_OrganizationID", + table: "InviteLinks"); + + migrationBuilder.DropForeignKey( + name: "FK_RemotelyUsers_Organizations_OrganizationID", + table: "RemotelyUsers"); + + migrationBuilder.DropForeignKey( + name: "FK_SavedScripts_Organizations_OrganizationID", + table: "SavedScripts"); + + migrationBuilder.DropForeignKey( + name: "FK_SavedScripts_RemotelyUsers_CreatorId", + table: "SavedScripts"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptResults_Devices_DeviceID", + table: "ScriptResults"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptResults_Organizations_OrganizationID", + table: "ScriptResults"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptResults_SavedScripts_SavedScriptId", + table: "ScriptResults"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptRuns_Organizations_OrganizationID", + table: "ScriptRuns"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptRuns_SavedScripts_SavedScriptId", + table: "ScriptRuns"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptRuns_ScriptSchedules_ScheduleId", + table: "ScriptRuns"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptSchedules_Organizations_OrganizationID", + table: "ScriptSchedules"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptSchedules_RemotelyUsers_CreatorId", + table: "ScriptSchedules"); + + migrationBuilder.DropIndex( + name: "IX_ScriptRuns_SavedScriptId", + table: "ScriptRuns"); + + migrationBuilder.DropIndex( + name: "IX_ScriptRuns_ScheduleId", + table: "ScriptRuns"); + + migrationBuilder.DropIndex( + name: "IX_ScriptResults_SavedScriptId", + table: "ScriptResults"); + + migrationBuilder.DropIndex( + name: "IX_BrandingInfos_OrganizationId", + table: "BrandingInfos"); + + migrationBuilder.AlterColumn( + name: "FileContents", + table: "SharedFiles", + type: "varbinary(max)", + nullable: true, + oldClrType: typeof(byte[]), + oldType: "varbinary(max)"); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "ScriptSchedules", + type: "nvarchar(450)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(450)"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "ScriptSchedules", + type: "nvarchar(max)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(max)"); + + migrationBuilder.AlterColumn( + name: "CreatorId", + table: "ScriptSchedules", + type: "nvarchar(450)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(450)"); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "ScriptRuns", + type: "nvarchar(450)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(450)"); + + migrationBuilder.AddColumn( + name: "ScriptScheduleId", + table: "ScriptRuns", + type: "int", + nullable: true); + + migrationBuilder.AlterColumn( + name: "ScriptInput", + table: "ScriptResults", + type: "nvarchar(max)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(max)"); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "ScriptResults", + type: "nvarchar(450)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(450)"); + + migrationBuilder.AlterColumn( + name: "DeviceID", + table: "ScriptResults", + type: "nvarchar(450)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(450)"); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "SavedScripts", + type: "nvarchar(450)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(450)"); + + migrationBuilder.AlterColumn( + name: "CreatorId", + table: "SavedScripts", + type: "nvarchar(450)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(450)"); + + migrationBuilder.AlterColumn( + name: "OrganizationName", + table: "Organizations", + type: "nvarchar(25)", + maxLength: 25, + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(25)", + oldMaxLength: 25); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "InviteLinks", + type: "nvarchar(450)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(450)"); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "Devices", + type: "nvarchar(450)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(450)"); + + migrationBuilder.AlterColumn( + name: "MacAddresses", + table: "Devices", + type: "nvarchar(max)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(max)"); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "DeviceGroups", + type: "nvarchar(450)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(450)"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "DeviceGroups", + type: "nvarchar(200)", + maxLength: 200, + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(200)", + oldMaxLength: 200); + + migrationBuilder.AlterColumn( + name: "OrganizationId", + table: "BrandingInfos", + type: "nvarchar(450)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(450)"); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "ApiTokens", + type: "nvarchar(450)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(450)"); + + migrationBuilder.AlterColumn( + name: "UserID", + table: "Alerts", + type: "nvarchar(450)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(450)"); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "Alerts", + type: "nvarchar(450)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(450)"); + + migrationBuilder.AlterColumn( + name: "DeviceID", + table: "Alerts", + type: "nvarchar(450)", + nullable: true, + oldClrType: typeof(string), + oldType: "nvarchar(450)"); + + migrationBuilder.CreateTable( + name: "DeviceScriptRun1", + columns: table => new + { + DevicesCompletedID = table.Column(type: "nvarchar(450)", nullable: false), + ScriptRunsCompletedId = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_DeviceScriptRun1", x => new { x.DevicesCompletedID, x.ScriptRunsCompletedId }); + table.ForeignKey( + name: "FK_DeviceScriptRun1_Devices_DevicesCompletedID", + column: x => x.DevicesCompletedID, + principalTable: "Devices", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_DeviceScriptRun1_ScriptRuns_ScriptRunsCompletedId", + column: x => x.ScriptRunsCompletedId, + principalTable: "ScriptRuns", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ScriptRuns_ScriptScheduleId", + table: "ScriptRuns", + column: "ScriptScheduleId"); + + migrationBuilder.CreateIndex( + name: "IX_BrandingInfos_OrganizationId", + table: "BrandingInfos", + column: "OrganizationId", + unique: true, + filter: "[OrganizationId] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_DeviceScriptRun1_ScriptRunsCompletedId", + table: "DeviceScriptRun1", + column: "ScriptRunsCompletedId"); + + migrationBuilder.AddForeignKey( + name: "FK_Alerts_Devices_DeviceID", + table: "Alerts", + column: "DeviceID", + principalTable: "Devices", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_Alerts_Organizations_OrganizationID", + table: "Alerts", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_Alerts_RemotelyUsers_UserID", + table: "Alerts", + column: "UserID", + principalTable: "RemotelyUsers", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_ApiTokens_Organizations_OrganizationID", + table: "ApiTokens", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_BrandingInfos_Organizations_OrganizationId", + table: "BrandingInfos", + column: "OrganizationId", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_DeviceGroups_Organizations_OrganizationID", + table: "DeviceGroups", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_Devices_Organizations_OrganizationID", + table: "Devices", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_InviteLinks_Organizations_OrganizationID", + table: "InviteLinks", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_RemotelyUsers_Organizations_OrganizationID", + table: "RemotelyUsers", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_SavedScripts_Organizations_OrganizationID", + table: "SavedScripts", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_SavedScripts_RemotelyUsers_CreatorId", + table: "SavedScripts", + column: "CreatorId", + principalTable: "RemotelyUsers", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptResults_Devices_DeviceID", + table: "ScriptResults", + column: "DeviceID", + principalTable: "Devices", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptResults_Organizations_OrganizationID", + table: "ScriptResults", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptRuns_Organizations_OrganizationID", + table: "ScriptRuns", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptRuns_ScriptSchedules_ScriptScheduleId", + table: "ScriptRuns", + column: "ScriptScheduleId", + principalTable: "ScriptSchedules", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptSchedules_Organizations_OrganizationID", + table: "ScriptSchedules", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptSchedules_RemotelyUsers_CreatorId", + table: "ScriptSchedules", + column: "CreatorId", + principalTable: "RemotelyUsers", + principalColumn: "Id"); + } +} diff --git a/Server/Migrations/SqlServer/SqlServerDbContextModelSnapshot.cs b/Server/Migrations/SqlServer/SqlServerDbContextModelSnapshot.cs index 24a0740bb..fd204c8f4 100644 --- a/Server/Migrations/SqlServer/SqlServerDbContextModelSnapshot.cs +++ b/Server/Migrations/SqlServer/SqlServerDbContextModelSnapshot.cs @@ -67,21 +67,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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") @@ -303,7 +288,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AspNetUserTokens", (string)null); }); - modelBuilder.Entity("Remotely.Shared.Models.Alert", b => + modelBuilder.Entity("Remotely.Shared.Entities.Alert", b => { b.Property("ID") .ValueGeneratedOnAdd() @@ -316,15 +301,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(max)"); b.Property("DeviceID") + .IsRequired() .HasColumnType("nvarchar(450)"); b.Property("Message") .HasColumnType("nvarchar(max)"); b.Property("OrganizationID") + .IsRequired() .HasColumnType("nvarchar(450)"); b.Property("UserID") + .IsRequired() .HasColumnType("nvarchar(450)"); b.HasKey("ID"); @@ -338,7 +326,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Alerts"); }); - modelBuilder.Entity("Remotely.Shared.Models.ApiToken", b => + modelBuilder.Entity("Remotely.Shared.Entities.ApiToken", b => { b.Property("ID") .ValueGeneratedOnAdd() @@ -352,6 +340,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(200)"); b.Property("OrganizationID") + .IsRequired() .HasColumnType("nvarchar(450)"); b.Property("Secret") @@ -364,7 +353,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ApiTokens"); }); - modelBuilder.Entity("Remotely.Shared.Models.BrandingInfo", b => + modelBuilder.Entity("Remotely.Shared.Entities.BrandingInfo", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -384,6 +373,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("varbinary(max)"); b.Property("OrganizationId") + .IsRequired() .HasColumnType("nvarchar(450)"); b.Property("Product") @@ -412,13 +402,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); b.HasIndex("OrganizationId") - .IsUnique() - .HasFilter("[OrganizationId] IS NOT NULL"); + .IsUnique(); b.ToTable("BrandingInfos"); }); - modelBuilder.Entity("Remotely.Shared.Models.Device", b => + modelBuilder.Entity("Remotely.Shared.Entities.Device", b => { b.Property("ID") .HasColumnType("nvarchar(450)"); @@ -455,6 +444,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("datetimeoffset"); b.Property("MacAddresses") + .IsRequired() .HasColumnType("nvarchar(max)"); b.Property("Notes") @@ -468,6 +458,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(max)"); b.Property("OrganizationID") + .IsRequired() .HasColumnType("nvarchar(450)"); b.Property("Platform") @@ -509,27 +500,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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 => + modelBuilder.Entity("Remotely.Shared.Entities.InviteLink", b => { b.Property("ID") .ValueGeneratedOnAdd() @@ -545,6 +516,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("bit"); b.Property("OrganizationID") + .IsRequired() .HasColumnType("nvarchar(450)"); b.Property("ResetUrl") @@ -557,7 +529,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("InviteLinks"); }); - modelBuilder.Entity("Remotely.Shared.Models.Organization", b => + modelBuilder.Entity("Remotely.Shared.Entities.Organization", b => { b.Property("ID") .ValueGeneratedOnAdd() @@ -567,6 +539,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("bit"); b.Property("OrganizationName") + .IsRequired() .HasMaxLength(25) .HasColumnType("nvarchar(25)"); @@ -575,7 +548,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Organizations"); }); - modelBuilder.Entity("Remotely.Shared.Models.SavedScript", b => + modelBuilder.Entity("Remotely.Shared.Entities.SavedScript", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -586,6 +559,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(max)"); b.Property("CreatorId") + .IsRequired() .HasColumnType("nvarchar(450)"); b.Property("FolderPath") @@ -607,6 +581,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(100)"); b.Property("OrganizationID") + .IsRequired() .HasColumnType("nvarchar(450)"); b.Property("SendEmailOnError") @@ -627,13 +602,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("SavedScripts"); }); - modelBuilder.Entity("Remotely.Shared.Models.ScriptResult", b => + modelBuilder.Entity("Remotely.Shared.Entities.ScriptResult", b => { b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("nvarchar(450)"); b.Property("DeviceID") + .IsRequired() .HasColumnType("nvarchar(450)"); b.Property("ErrorOutput") @@ -646,6 +622,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("OrganizationID") + .IsRequired() .HasColumnType("nvarchar(450)"); b.Property("RunTime") @@ -658,6 +635,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("ScriptInput") + .IsRequired() .HasColumnType("nvarchar(max)"); b.Property("ScriptRunId") @@ -684,6 +662,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("OrganizationID"); + b.HasIndex("SavedScriptId"); + b.HasIndex("ScheduleId"); b.HasIndex("ScriptRunId"); @@ -691,7 +671,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ScriptResults"); }); - modelBuilder.Entity("Remotely.Shared.Models.ScriptRun", b => + modelBuilder.Entity("Remotely.Shared.Entities.ScriptRun", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -706,6 +686,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("int"); b.Property("OrganizationID") + .IsRequired() .HasColumnType("nvarchar(450)"); b.Property("RunAt") @@ -720,19 +701,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ScheduleId") .HasColumnType("int"); - b.Property("ScriptScheduleId") - .HasColumnType("int"); - b.HasKey("Id"); b.HasIndex("OrganizationID"); - b.HasIndex("ScriptScheduleId"); + b.HasIndex("SavedScriptId"); + + b.HasIndex("ScheduleId"); b.ToTable("ScriptRuns"); }); - modelBuilder.Entity("Remotely.Shared.Models.ScriptSchedule", b => + modelBuilder.Entity("Remotely.Shared.Entities.ScriptSchedule", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -744,6 +724,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("datetimeoffset"); b.Property("CreatorId") + .IsRequired() .HasColumnType("nvarchar(450)"); b.Property("Interval") @@ -753,12 +734,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("datetimeoffset"); b.Property("Name") + .IsRequired() .HasColumnType("nvarchar(max)"); b.Property("NextRun") .HasColumnType("datetimeoffset"); b.Property("OrganizationID") + .IsRequired() .HasColumnType("nvarchar(450)"); b.Property("RunOnNextConnect") @@ -779,7 +762,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ScriptSchedules"); }); - modelBuilder.Entity("Remotely.Shared.Models.SharedFile", b => + modelBuilder.Entity("Remotely.Shared.Entities.SharedFile", b => { b.Property("ID") .ValueGeneratedOnAdd() @@ -789,6 +772,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("nvarchar(max)"); b.Property("FileContents") + .IsRequired() .HasColumnType("varbinary(max)"); b.Property("FileName") @@ -807,7 +791,29 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("SharedFiles"); }); - modelBuilder.Entity("Remotely.Shared.Models.RemotelyUser", b => + modelBuilder.Entity("Remotely.Shared.Models.DeviceGroup", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OrganizationID") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("ID"); + + b.HasIndex("OrganizationID"); + + b.ToTable("DeviceGroups"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.RemotelyUser", b => { b.HasBaseType("Microsoft.AspNetCore.Identity.IdentityUser"); @@ -818,6 +824,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("bit"); b.Property("OrganizationID") + .IsRequired() .HasColumnType("nvarchar(450)"); b.Property("TempPassword") @@ -841,7 +848,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Remotely.Shared.Models.RemotelyUser", null) + b.HasOne("Remotely.Shared.Entities.RemotelyUser", null) .WithMany() .HasForeignKey("UsersId") .OnDelete(DeleteBehavior.Cascade) @@ -856,7 +863,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Remotely.Shared.Models.ScriptSchedule", null) + b.HasOne("Remotely.Shared.Entities.ScriptSchedule", null) .WithMany() .HasForeignKey("ScriptSchedulesId") .OnDelete(DeleteBehavior.Cascade) @@ -865,43 +872,28 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("DeviceScriptRun", b => { - b.HasOne("Remotely.Shared.Models.Device", null) + b.HasOne("Remotely.Shared.Entities.Device", null) .WithMany() .HasForeignKey("DevicesID") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Remotely.Shared.Models.ScriptRun", null) + b.HasOne("Remotely.Shared.Entities.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) + b.HasOne("Remotely.Shared.Entities.Device", null) .WithMany() .HasForeignKey("DevicesID") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Remotely.Shared.Models.ScriptSchedule", null) + b.HasOne("Remotely.Shared.Entities.ScriptSchedule", null) .WithMany() .HasForeignKey("ScriptSchedulesId") .OnDelete(DeleteBehavior.Cascade) @@ -959,19 +951,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); - modelBuilder.Entity("Remotely.Shared.Models.Alert", b => + modelBuilder.Entity("Remotely.Shared.Entities.Alert", b => { - b.HasOne("Remotely.Shared.Models.Device", "Device") + b.HasOne("Remotely.Shared.Entities.Device", "Device") .WithMany("Alerts") - .HasForeignKey("DeviceID"); + .HasForeignKey("DeviceID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithMany("Alerts") - .HasForeignKey("OrganizationID"); + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Remotely.Shared.Models.RemotelyUser", "User") + b.HasOne("Remotely.Shared.Entities.RemotelyUser", "User") .WithMany("Alerts") - .HasForeignKey("UserID"); + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); b.Navigation("Device"); @@ -980,87 +978,98 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); - modelBuilder.Entity("Remotely.Shared.Models.ApiToken", b => + modelBuilder.Entity("Remotely.Shared.Entities.ApiToken", b => { - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithMany("ApiTokens") - .HasForeignKey("OrganizationID"); + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); b.Navigation("Organization"); }); - modelBuilder.Entity("Remotely.Shared.Models.BrandingInfo", b => + modelBuilder.Entity("Remotely.Shared.Entities.BrandingInfo", b => { - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithOne("BrandingInfo") - .HasForeignKey("Remotely.Shared.Models.BrandingInfo", "OrganizationId"); + .HasForeignKey("Remotely.Shared.Entities.BrandingInfo", "OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); b.Navigation("Organization"); }); - modelBuilder.Entity("Remotely.Shared.Models.Device", b => + modelBuilder.Entity("Remotely.Shared.Entities.Device", b => { b.HasOne("Remotely.Shared.Models.DeviceGroup", "DeviceGroup") .WithMany("Devices") .HasForeignKey("DeviceGroupID"); - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithMany("Devices") - .HasForeignKey("OrganizationID"); + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); 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 => + modelBuilder.Entity("Remotely.Shared.Entities.InviteLink", b => { - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithMany("InviteLinks") - .HasForeignKey("OrganizationID"); + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); b.Navigation("Organization"); }); - modelBuilder.Entity("Remotely.Shared.Models.SavedScript", b => + modelBuilder.Entity("Remotely.Shared.Entities.SavedScript", b => { - b.HasOne("Remotely.Shared.Models.RemotelyUser", "Creator") + b.HasOne("Remotely.Shared.Entities.RemotelyUser", "Creator") .WithMany("SavedScripts") - .HasForeignKey("CreatorId"); + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithMany("SavedScripts") - .HasForeignKey("OrganizationID"); + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); b.Navigation("Creator"); b.Navigation("Organization"); }); - modelBuilder.Entity("Remotely.Shared.Models.ScriptResult", b => + modelBuilder.Entity("Remotely.Shared.Entities.ScriptResult", b => { - b.HasOne("Remotely.Shared.Models.Device", "Device") + b.HasOne("Remotely.Shared.Entities.Device", "Device") .WithMany("ScriptResults") - .HasForeignKey("DeviceID"); + .HasForeignKey("DeviceID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithMany("ScriptResults") - .HasForeignKey("OrganizationID"); + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.SavedScript", "SavedScript") + .WithMany("ScriptResults") + .HasForeignKey("SavedScriptId"); - b.HasOne("Remotely.Shared.Models.ScriptSchedule", "Schedule") + b.HasOne("Remotely.Shared.Entities.ScriptSchedule", "Schedule") .WithMany() .HasForeignKey("ScheduleId"); - b.HasOne("Remotely.Shared.Models.ScriptRun", null) + b.HasOne("Remotely.Shared.Entities.ScriptRun", "ScriptRun") .WithMany("Results") .HasForeignKey("ScriptRunId"); @@ -1068,68 +1077,94 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Organization"); + b.Navigation("SavedScript"); + b.Navigation("Schedule"); + + b.Navigation("ScriptRun"); }); - modelBuilder.Entity("Remotely.Shared.Models.ScriptRun", b => + modelBuilder.Entity("Remotely.Shared.Entities.ScriptRun", b => { - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithMany("ScriptRuns") - .HasForeignKey("OrganizationID"); + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Remotely.Shared.Models.ScriptSchedule", null) + b.HasOne("Remotely.Shared.Entities.SavedScript", "SavedScript") .WithMany("ScriptRuns") - .HasForeignKey("ScriptScheduleId"); + .HasForeignKey("SavedScriptId"); + + b.HasOne("Remotely.Shared.Entities.ScriptSchedule", "Schedule") + .WithMany("ScriptRuns") + .HasForeignKey("ScheduleId"); b.Navigation("Organization"); + + b.Navigation("SavedScript"); + + b.Navigation("Schedule"); }); - modelBuilder.Entity("Remotely.Shared.Models.ScriptSchedule", b => + modelBuilder.Entity("Remotely.Shared.Entities.ScriptSchedule", b => { - b.HasOne("Remotely.Shared.Models.RemotelyUser", "Creator") + b.HasOne("Remotely.Shared.Entities.RemotelyUser", "Creator") .WithMany("ScriptSchedules") - .HasForeignKey("CreatorId"); + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithMany("ScriptSchedules") - .HasForeignKey("OrganizationID"); + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); b.Navigation("Creator"); b.Navigation("Organization"); }); - modelBuilder.Entity("Remotely.Shared.Models.SharedFile", b => + modelBuilder.Entity("Remotely.Shared.Entities.SharedFile", b => { - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithMany("SharedFiles") .HasForeignKey("OrganizationID"); b.Navigation("Organization"); }); - modelBuilder.Entity("Remotely.Shared.Models.RemotelyUser", b => + modelBuilder.Entity("Remotely.Shared.Models.DeviceGroup", b => { - b.HasOne("Remotely.Shared.Models.Organization", "Organization") - .WithMany("RemotelyUsers") - .HasForeignKey("OrganizationID"); + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("DeviceGroups") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); b.Navigation("Organization"); }); - modelBuilder.Entity("Remotely.Shared.Models.Device", b => + modelBuilder.Entity("Remotely.Shared.Entities.RemotelyUser", b => { - b.Navigation("Alerts"); + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("RemotelyUsers") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.Navigation("ScriptResults"); + b.Navigation("Organization"); }); - modelBuilder.Entity("Remotely.Shared.Models.DeviceGroup", b => + modelBuilder.Entity("Remotely.Shared.Entities.Device", b => { - b.Navigation("Devices"); + b.Navigation("Alerts"); + + b.Navigation("ScriptResults"); }); - modelBuilder.Entity("Remotely.Shared.Models.Organization", b => + modelBuilder.Entity("Remotely.Shared.Entities.Organization", b => { b.Navigation("Alerts"); @@ -1156,17 +1191,29 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("SharedFiles"); }); - modelBuilder.Entity("Remotely.Shared.Models.ScriptRun", b => + modelBuilder.Entity("Remotely.Shared.Entities.SavedScript", b => + { + b.Navigation("ScriptResults"); + + b.Navigation("ScriptRuns"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.ScriptRun", b => { b.Navigation("Results"); }); - modelBuilder.Entity("Remotely.Shared.Models.ScriptSchedule", b => + modelBuilder.Entity("Remotely.Shared.Entities.ScriptSchedule", b => { b.Navigation("ScriptRuns"); }); - modelBuilder.Entity("Remotely.Shared.Models.RemotelyUser", b => + modelBuilder.Entity("Remotely.Shared.Models.DeviceGroup", b => + { + b.Navigation("Devices"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.RemotelyUser", b => { b.Navigation("Alerts"); diff --git a/Server/Migrations/Sqlite/20230726225800_Enable_NullableReferences.Designer.cs b/Server/Migrations/Sqlite/20230726225800_Enable_NullableReferences.Designer.cs new file mode 100644 index 000000000..6352e1b05 --- /dev/null +++ b/Server/Migrations/Sqlite/20230726225800_Enable_NullableReferences.Designer.cs @@ -0,0 +1,1224 @@ +// +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("20230726225800_Enable_NullableReferences")] + partial class Enable_NullableReferences + { + /// + 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("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.Entities.Alert", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedOn") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Details") + .HasColumnType("TEXT"); + + b.Property("DeviceID") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("OrganizationID") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserID") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("DeviceID"); + + b.HasIndex("OrganizationID"); + + b.HasIndex("UserID"); + + b.ToTable("Alerts"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.ApiToken", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("LastUsed") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationID") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Secret") + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("OrganizationID"); + + b.ToTable("ApiTokens"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.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") + .IsRequired() + .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.Entities.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") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(5000) + .HasColumnType("TEXT"); + + b.Property("OSArchitecture") + .HasColumnType("INTEGER"); + + b.Property("OSDescription") + .HasColumnType("TEXT"); + + b.Property("OrganizationID") + .IsRequired() + .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.Entities.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") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ResetUrl") + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("OrganizationID"); + + b.ToTable("InviteLinks"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.Organization", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsDefaultOrganization") + .HasColumnType("INTEGER"); + + b.Property("OrganizationName") + .IsRequired() + .HasMaxLength(25) + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.ToTable("Organizations"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.SavedScript", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatorId") + .IsRequired() + .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") + .IsRequired() + .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.Entities.ScriptResult", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("DeviceID") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ErrorOutput") + .HasColumnType("TEXT"); + + b.Property("HadErrors") + .HasColumnType("INTEGER"); + + b.Property("InputType") + .HasColumnType("INTEGER"); + + b.Property("OrganizationID") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RunTime") + .HasColumnType("TEXT"); + + b.Property("SavedScriptId") + .HasColumnType("TEXT"); + + b.Property("ScheduleId") + .HasColumnType("INTEGER"); + + b.Property("ScriptInput") + .IsRequired() + .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("SavedScriptId"); + + b.HasIndex("ScheduleId"); + + b.HasIndex("ScriptRunId"); + + b.ToTable("ScriptResults"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.ScriptRun", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Initiator") + .HasColumnType("TEXT"); + + b.Property("InputType") + .HasColumnType("INTEGER"); + + b.Property("OrganizationID") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RunAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("RunOnNextConnect") + .HasColumnType("INTEGER"); + + b.Property("SavedScriptId") + .HasColumnType("TEXT"); + + b.Property("ScheduleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationID"); + + b.HasIndex("SavedScriptId"); + + b.HasIndex("ScheduleId"); + + b.ToTable("ScriptRuns"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.ScriptSchedule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatorId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Interval") + .HasColumnType("INTEGER"); + + b.Property("LastRun") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("NextRun") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationID") + .IsRequired() + .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.Entities.SharedFile", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ContentType") + .HasColumnType("TEXT"); + + b.Property("FileContents") + .IsRequired() + .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.DeviceGroup", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationID") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("OrganizationID"); + + b.ToTable("DeviceGroups"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.RemotelyUser", b => + { + b.HasBaseType("Microsoft.AspNetCore.Identity.IdentityUser"); + + b.Property("IsAdministrator") + .HasColumnType("INTEGER"); + + b.Property("IsServerAdmin") + .HasColumnType("INTEGER"); + + b.Property("OrganizationID") + .IsRequired() + .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.Entities.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.Entities.ScriptSchedule", null) + .WithMany() + .HasForeignKey("ScriptSchedulesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("DeviceScriptRun", b => + { + b.HasOne("Remotely.Shared.Entities.Device", null) + .WithMany() + .HasForeignKey("DevicesID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.ScriptRun", null) + .WithMany() + .HasForeignKey("ScriptRunsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("DeviceScriptSchedule", b => + { + b.HasOne("Remotely.Shared.Entities.Device", null) + .WithMany() + .HasForeignKey("DevicesID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.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.Entities.Alert", b => + { + b.HasOne("Remotely.Shared.Entities.Device", "Device") + .WithMany("Alerts") + .HasForeignKey("DeviceID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("Alerts") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.RemotelyUser", "User") + .WithMany("Alerts") + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Device"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.ApiToken", b => + { + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("ApiTokens") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.BrandingInfo", b => + { + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithOne("BrandingInfo") + .HasForeignKey("Remotely.Shared.Entities.BrandingInfo", "OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.Device", b => + { + b.HasOne("Remotely.Shared.Models.DeviceGroup", "DeviceGroup") + .WithMany("Devices") + .HasForeignKey("DeviceGroupID"); + + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("Devices") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DeviceGroup"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.InviteLink", b => + { + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("InviteLinks") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.SavedScript", b => + { + b.HasOne("Remotely.Shared.Entities.RemotelyUser", "Creator") + .WithMany("SavedScripts") + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("SavedScripts") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.ScriptResult", b => + { + b.HasOne("Remotely.Shared.Entities.Device", "Device") + .WithMany("ScriptResults") + .HasForeignKey("DeviceID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("ScriptResults") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.SavedScript", "SavedScript") + .WithMany("ScriptResults") + .HasForeignKey("SavedScriptId"); + + b.HasOne("Remotely.Shared.Entities.ScriptSchedule", "Schedule") + .WithMany() + .HasForeignKey("ScheduleId"); + + b.HasOne("Remotely.Shared.Entities.ScriptRun", "ScriptRun") + .WithMany("Results") + .HasForeignKey("ScriptRunId"); + + b.Navigation("Device"); + + b.Navigation("Organization"); + + b.Navigation("SavedScript"); + + b.Navigation("Schedule"); + + b.Navigation("ScriptRun"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.ScriptRun", b => + { + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("ScriptRuns") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.SavedScript", "SavedScript") + .WithMany("ScriptRuns") + .HasForeignKey("SavedScriptId"); + + b.HasOne("Remotely.Shared.Entities.ScriptSchedule", "Schedule") + .WithMany("ScriptRuns") + .HasForeignKey("ScheduleId"); + + b.Navigation("Organization"); + + b.Navigation("SavedScript"); + + b.Navigation("Schedule"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.ScriptSchedule", b => + { + b.HasOne("Remotely.Shared.Entities.RemotelyUser", "Creator") + .WithMany("ScriptSchedules") + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("ScriptSchedules") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.SharedFile", b => + { + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("SharedFiles") + .HasForeignKey("OrganizationID"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.DeviceGroup", b => + { + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("DeviceGroups") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.RemotelyUser", b => + { + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("RemotelyUsers") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.Device", b => + { + b.Navigation("Alerts"); + + b.Navigation("ScriptResults"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.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.Entities.SavedScript", b => + { + b.Navigation("ScriptResults"); + + b.Navigation("ScriptRuns"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.ScriptRun", b => + { + b.Navigation("Results"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.ScriptSchedule", b => + { + b.Navigation("ScriptRuns"); + }); + + modelBuilder.Entity("Remotely.Shared.Models.DeviceGroup", b => + { + b.Navigation("Devices"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.RemotelyUser", b => + { + b.Navigation("Alerts"); + + b.Navigation("SavedScripts"); + + b.Navigation("ScriptSchedules"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Server/Migrations/Sqlite/20230726225800_Enable_NullableReferences.cs b/Server/Migrations/Sqlite/20230726225800_Enable_NullableReferences.cs new file mode 100644 index 000000000..b5103639c --- /dev/null +++ b/Server/Migrations/Sqlite/20230726225800_Enable_NullableReferences.cs @@ -0,0 +1,896 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Remotely.Server.Migrations.Sqlite; + +/// +public partial class Enable_NullableReferences : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // This relationship wasn't enforced previously. + migrationBuilder.Sql("delete from ScriptResults where DeviceID is null;"); + + migrationBuilder.DropForeignKey( + name: "FK_Alerts_Devices_DeviceID", + table: "Alerts"); + + migrationBuilder.DropForeignKey( + name: "FK_Alerts_Organizations_OrganizationID", + table: "Alerts"); + + migrationBuilder.DropForeignKey( + name: "FK_Alerts_RemotelyUsers_UserID", + table: "Alerts"); + + migrationBuilder.DropForeignKey( + name: "FK_ApiTokens_Organizations_OrganizationID", + table: "ApiTokens"); + + migrationBuilder.DropForeignKey( + name: "FK_BrandingInfos_Organizations_OrganizationId", + table: "BrandingInfos"); + + migrationBuilder.DropForeignKey( + name: "FK_DeviceGroups_Organizations_OrganizationID", + table: "DeviceGroups"); + + migrationBuilder.DropForeignKey( + name: "FK_Devices_Organizations_OrganizationID", + table: "Devices"); + + migrationBuilder.DropForeignKey( + name: "FK_InviteLinks_Organizations_OrganizationID", + table: "InviteLinks"); + + migrationBuilder.DropForeignKey( + name: "FK_RemotelyUsers_Organizations_OrganizationID", + table: "RemotelyUsers"); + + migrationBuilder.DropForeignKey( + name: "FK_SavedScripts_Organizations_OrganizationID", + table: "SavedScripts"); + + migrationBuilder.DropForeignKey( + name: "FK_SavedScripts_RemotelyUsers_CreatorId", + table: "SavedScripts"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptResults_Devices_DeviceID", + table: "ScriptResults"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptResults_Organizations_OrganizationID", + table: "ScriptResults"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptRuns_Organizations_OrganizationID", + table: "ScriptRuns"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptRuns_ScriptSchedules_ScriptScheduleId", + table: "ScriptRuns"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptSchedules_Organizations_OrganizationID", + table: "ScriptSchedules"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptSchedules_RemotelyUsers_CreatorId", + table: "ScriptSchedules"); + + migrationBuilder.DropTable( + name: "DeviceScriptRun1"); + + migrationBuilder.DropIndex( + name: "IX_ScriptRuns_ScriptScheduleId", + table: "ScriptRuns"); + + migrationBuilder.DropColumn( + name: "ScriptScheduleId", + table: "ScriptRuns"); + + migrationBuilder.AlterColumn( + name: "FileContents", + table: "SharedFiles", + type: "BLOB", + nullable: false, + defaultValue: new byte[0], + oldClrType: typeof(byte[]), + oldType: "BLOB", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "ScriptSchedules", + type: "TEXT", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Name", + table: "ScriptSchedules", + type: "TEXT", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatorId", + table: "ScriptSchedules", + type: "TEXT", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "ScriptRuns", + type: "TEXT", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "ScriptInput", + table: "ScriptResults", + type: "TEXT", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "ScriptResults", + type: "TEXT", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeviceID", + table: "ScriptResults", + type: "TEXT", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "SavedScripts", + type: "TEXT", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "CreatorId", + table: "SavedScripts", + type: "TEXT", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationName", + table: "Organizations", + type: "TEXT", + maxLength: 25, + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "TEXT", + oldMaxLength: 25, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "InviteLinks", + type: "TEXT", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "Devices", + type: "TEXT", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "MacAddresses", + table: "Devices", + type: "TEXT", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "DeviceGroups", + type: "TEXT", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Name", + table: "DeviceGroups", + type: "TEXT", + maxLength: 200, + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "TEXT", + oldMaxLength: 200, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationId", + table: "BrandingInfos", + type: "TEXT", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "ApiTokens", + type: "TEXT", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UserID", + table: "Alerts", + type: "TEXT", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "Alerts", + type: "TEXT", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DeviceID", + table: "Alerts", + type: "TEXT", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "TEXT", + oldNullable: true); + + migrationBuilder.CreateIndex( + name: "IX_ScriptRuns_SavedScriptId", + table: "ScriptRuns", + column: "SavedScriptId"); + + migrationBuilder.CreateIndex( + name: "IX_ScriptRuns_ScheduleId", + table: "ScriptRuns", + column: "ScheduleId"); + + migrationBuilder.CreateIndex( + name: "IX_ScriptResults_SavedScriptId", + table: "ScriptResults", + column: "SavedScriptId"); + + migrationBuilder.AddForeignKey( + name: "FK_Alerts_Devices_DeviceID", + table: "Alerts", + column: "DeviceID", + principalTable: "Devices", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Alerts_Organizations_OrganizationID", + table: "Alerts", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Alerts_RemotelyUsers_UserID", + table: "Alerts", + column: "UserID", + principalTable: "RemotelyUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_ApiTokens_Organizations_OrganizationID", + table: "ApiTokens", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_BrandingInfos_Organizations_OrganizationId", + table: "BrandingInfos", + column: "OrganizationId", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_DeviceGroups_Organizations_OrganizationID", + table: "DeviceGroups", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Devices_Organizations_OrganizationID", + table: "Devices", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_InviteLinks_Organizations_OrganizationID", + table: "InviteLinks", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_RemotelyUsers_Organizations_OrganizationID", + table: "RemotelyUsers", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_SavedScripts_Organizations_OrganizationID", + table: "SavedScripts", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_SavedScripts_RemotelyUsers_CreatorId", + table: "SavedScripts", + column: "CreatorId", + principalTable: "RemotelyUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptResults_Devices_DeviceID", + table: "ScriptResults", + column: "DeviceID", + principalTable: "Devices", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptResults_Organizations_OrganizationID", + table: "ScriptResults", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptResults_SavedScripts_SavedScriptId", + table: "ScriptResults", + column: "SavedScriptId", + principalTable: "SavedScripts", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptRuns_Organizations_OrganizationID", + table: "ScriptRuns", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptRuns_SavedScripts_SavedScriptId", + table: "ScriptRuns", + column: "SavedScriptId", + principalTable: "SavedScripts", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptRuns_ScriptSchedules_ScheduleId", + table: "ScriptRuns", + column: "ScheduleId", + principalTable: "ScriptSchedules", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptSchedules_Organizations_OrganizationID", + table: "ScriptSchedules", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptSchedules_RemotelyUsers_CreatorId", + table: "ScriptSchedules", + column: "CreatorId", + principalTable: "RemotelyUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Alerts_Devices_DeviceID", + table: "Alerts"); + + migrationBuilder.DropForeignKey( + name: "FK_Alerts_Organizations_OrganizationID", + table: "Alerts"); + + migrationBuilder.DropForeignKey( + name: "FK_Alerts_RemotelyUsers_UserID", + table: "Alerts"); + + migrationBuilder.DropForeignKey( + name: "FK_ApiTokens_Organizations_OrganizationID", + table: "ApiTokens"); + + migrationBuilder.DropForeignKey( + name: "FK_BrandingInfos_Organizations_OrganizationId", + table: "BrandingInfos"); + + migrationBuilder.DropForeignKey( + name: "FK_DeviceGroups_Organizations_OrganizationID", + table: "DeviceGroups"); + + migrationBuilder.DropForeignKey( + name: "FK_Devices_Organizations_OrganizationID", + table: "Devices"); + + migrationBuilder.DropForeignKey( + name: "FK_InviteLinks_Organizations_OrganizationID", + table: "InviteLinks"); + + migrationBuilder.DropForeignKey( + name: "FK_RemotelyUsers_Organizations_OrganizationID", + table: "RemotelyUsers"); + + migrationBuilder.DropForeignKey( + name: "FK_SavedScripts_Organizations_OrganizationID", + table: "SavedScripts"); + + migrationBuilder.DropForeignKey( + name: "FK_SavedScripts_RemotelyUsers_CreatorId", + table: "SavedScripts"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptResults_Devices_DeviceID", + table: "ScriptResults"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptResults_Organizations_OrganizationID", + table: "ScriptResults"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptResults_SavedScripts_SavedScriptId", + table: "ScriptResults"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptRuns_Organizations_OrganizationID", + table: "ScriptRuns"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptRuns_SavedScripts_SavedScriptId", + table: "ScriptRuns"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptRuns_ScriptSchedules_ScheduleId", + table: "ScriptRuns"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptSchedules_Organizations_OrganizationID", + table: "ScriptSchedules"); + + migrationBuilder.DropForeignKey( + name: "FK_ScriptSchedules_RemotelyUsers_CreatorId", + table: "ScriptSchedules"); + + migrationBuilder.DropIndex( + name: "IX_ScriptRuns_SavedScriptId", + table: "ScriptRuns"); + + migrationBuilder.DropIndex( + name: "IX_ScriptRuns_ScheduleId", + table: "ScriptRuns"); + + migrationBuilder.DropIndex( + name: "IX_ScriptResults_SavedScriptId", + table: "ScriptResults"); + + migrationBuilder.AlterColumn( + name: "FileContents", + table: "SharedFiles", + type: "BLOB", + nullable: true, + oldClrType: typeof(byte[]), + oldType: "BLOB"); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "ScriptSchedules", + type: "TEXT", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "ScriptSchedules", + type: "TEXT", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT"); + + migrationBuilder.AlterColumn( + name: "CreatorId", + table: "ScriptSchedules", + type: "TEXT", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT"); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "ScriptRuns", + type: "TEXT", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT"); + + migrationBuilder.AddColumn( + name: "ScriptScheduleId", + table: "ScriptRuns", + type: "INTEGER", + nullable: true); + + migrationBuilder.AlterColumn( + name: "ScriptInput", + table: "ScriptResults", + type: "TEXT", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT"); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "ScriptResults", + type: "TEXT", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT"); + + migrationBuilder.AlterColumn( + name: "DeviceID", + table: "ScriptResults", + type: "TEXT", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT"); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "SavedScripts", + type: "TEXT", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT"); + + migrationBuilder.AlterColumn( + name: "CreatorId", + table: "SavedScripts", + type: "TEXT", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT"); + + migrationBuilder.AlterColumn( + name: "OrganizationName", + table: "Organizations", + type: "TEXT", + maxLength: 25, + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT", + oldMaxLength: 25); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "InviteLinks", + type: "TEXT", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT"); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "Devices", + type: "TEXT", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT"); + + migrationBuilder.AlterColumn( + name: "MacAddresses", + table: "Devices", + type: "TEXT", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT"); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "DeviceGroups", + type: "TEXT", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT"); + + migrationBuilder.AlterColumn( + name: "Name", + table: "DeviceGroups", + type: "TEXT", + maxLength: 200, + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT", + oldMaxLength: 200); + + migrationBuilder.AlterColumn( + name: "OrganizationId", + table: "BrandingInfos", + type: "TEXT", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT"); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "ApiTokens", + type: "TEXT", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT"); + + migrationBuilder.AlterColumn( + name: "UserID", + table: "Alerts", + type: "TEXT", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT"); + + migrationBuilder.AlterColumn( + name: "OrganizationID", + table: "Alerts", + type: "TEXT", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT"); + + migrationBuilder.AlterColumn( + name: "DeviceID", + table: "Alerts", + type: "TEXT", + nullable: true, + oldClrType: typeof(string), + oldType: "TEXT"); + + migrationBuilder.CreateTable( + name: "DeviceScriptRun1", + columns: table => new + { + DevicesCompletedID = table.Column(type: "TEXT", nullable: false), + ScriptRunsCompletedId = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_DeviceScriptRun1", x => new { x.DevicesCompletedID, x.ScriptRunsCompletedId }); + table.ForeignKey( + name: "FK_DeviceScriptRun1_Devices_DevicesCompletedID", + column: x => x.DevicesCompletedID, + principalTable: "Devices", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_DeviceScriptRun1_ScriptRuns_ScriptRunsCompletedId", + column: x => x.ScriptRunsCompletedId, + principalTable: "ScriptRuns", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ScriptRuns_ScriptScheduleId", + table: "ScriptRuns", + column: "ScriptScheduleId"); + + migrationBuilder.CreateIndex( + name: "IX_DeviceScriptRun1_ScriptRunsCompletedId", + table: "DeviceScriptRun1", + column: "ScriptRunsCompletedId"); + + migrationBuilder.AddForeignKey( + name: "FK_Alerts_Devices_DeviceID", + table: "Alerts", + column: "DeviceID", + principalTable: "Devices", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_Alerts_Organizations_OrganizationID", + table: "Alerts", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_Alerts_RemotelyUsers_UserID", + table: "Alerts", + column: "UserID", + principalTable: "RemotelyUsers", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_ApiTokens_Organizations_OrganizationID", + table: "ApiTokens", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_BrandingInfos_Organizations_OrganizationId", + table: "BrandingInfos", + column: "OrganizationId", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_DeviceGroups_Organizations_OrganizationID", + table: "DeviceGroups", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_Devices_Organizations_OrganizationID", + table: "Devices", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_InviteLinks_Organizations_OrganizationID", + table: "InviteLinks", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_RemotelyUsers_Organizations_OrganizationID", + table: "RemotelyUsers", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_SavedScripts_Organizations_OrganizationID", + table: "SavedScripts", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_SavedScripts_RemotelyUsers_CreatorId", + table: "SavedScripts", + column: "CreatorId", + principalTable: "RemotelyUsers", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptResults_Devices_DeviceID", + table: "ScriptResults", + column: "DeviceID", + principalTable: "Devices", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptResults_Organizations_OrganizationID", + table: "ScriptResults", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptRuns_Organizations_OrganizationID", + table: "ScriptRuns", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptRuns_ScriptSchedules_ScriptScheduleId", + table: "ScriptRuns", + column: "ScriptScheduleId", + principalTable: "ScriptSchedules", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptSchedules_Organizations_OrganizationID", + table: "ScriptSchedules", + column: "OrganizationID", + principalTable: "Organizations", + principalColumn: "ID"); + + migrationBuilder.AddForeignKey( + name: "FK_ScriptSchedules_RemotelyUsers_CreatorId", + table: "ScriptSchedules", + column: "CreatorId", + principalTable: "RemotelyUsers", + principalColumn: "Id"); + } +} diff --git a/Server/Migrations/Sqlite/SqliteDbContextModelSnapshot.cs b/Server/Migrations/Sqlite/SqliteDbContextModelSnapshot.cs index 3e9cb5ab2..28f1b51f8 100644 --- a/Server/Migrations/Sqlite/SqliteDbContextModelSnapshot.cs +++ b/Server/Migrations/Sqlite/SqliteDbContextModelSnapshot.cs @@ -62,21 +62,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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") @@ -292,7 +277,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AspNetUserTokens", (string)null); }); - modelBuilder.Entity("Remotely.Shared.Models.Alert", b => + modelBuilder.Entity("Remotely.Shared.Entities.Alert", b => { b.Property("ID") .ValueGeneratedOnAdd() @@ -306,15 +291,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("DeviceID") + .IsRequired() .HasColumnType("TEXT"); b.Property("Message") .HasColumnType("TEXT"); b.Property("OrganizationID") + .IsRequired() .HasColumnType("TEXT"); b.Property("UserID") + .IsRequired() .HasColumnType("TEXT"); b.HasKey("ID"); @@ -328,7 +316,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Alerts"); }); - modelBuilder.Entity("Remotely.Shared.Models.ApiToken", b => + modelBuilder.Entity("Remotely.Shared.Entities.ApiToken", b => { b.Property("ID") .ValueGeneratedOnAdd() @@ -342,6 +330,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("OrganizationID") + .IsRequired() .HasColumnType("TEXT"); b.Property("Secret") @@ -354,7 +343,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ApiTokens"); }); - modelBuilder.Entity("Remotely.Shared.Models.BrandingInfo", b => + modelBuilder.Entity("Remotely.Shared.Entities.BrandingInfo", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -374,6 +363,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("BLOB"); b.Property("OrganizationId") + .IsRequired() .HasColumnType("TEXT"); b.Property("Product") @@ -407,7 +397,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("BrandingInfos"); }); - modelBuilder.Entity("Remotely.Shared.Models.Device", b => + modelBuilder.Entity("Remotely.Shared.Entities.Device", b => { b.Property("ID") .HasColumnType("TEXT"); @@ -445,6 +435,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("MacAddresses") + .IsRequired() .HasColumnType("TEXT"); b.Property("Notes") @@ -458,6 +449,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("OrganizationID") + .IsRequired() .HasColumnType("TEXT"); b.Property("Platform") @@ -499,27 +491,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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 => + modelBuilder.Entity("Remotely.Shared.Entities.InviteLink", b => { b.Property("ID") .ValueGeneratedOnAdd() @@ -536,6 +508,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("INTEGER"); b.Property("OrganizationID") + .IsRequired() .HasColumnType("TEXT"); b.Property("ResetUrl") @@ -548,7 +521,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("InviteLinks"); }); - modelBuilder.Entity("Remotely.Shared.Models.Organization", b => + modelBuilder.Entity("Remotely.Shared.Entities.Organization", b => { b.Property("ID") .ValueGeneratedOnAdd() @@ -558,6 +531,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("INTEGER"); b.Property("OrganizationName") + .IsRequired() .HasMaxLength(25) .HasColumnType("TEXT"); @@ -566,7 +540,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Organizations"); }); - modelBuilder.Entity("Remotely.Shared.Models.SavedScript", b => + modelBuilder.Entity("Remotely.Shared.Entities.SavedScript", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -577,6 +551,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("CreatorId") + .IsRequired() .HasColumnType("TEXT"); b.Property("FolderPath") @@ -598,6 +573,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("OrganizationID") + .IsRequired() .HasColumnType("TEXT"); b.Property("SendEmailOnError") @@ -618,13 +594,14 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("SavedScripts"); }); - modelBuilder.Entity("Remotely.Shared.Models.ScriptResult", b => + modelBuilder.Entity("Remotely.Shared.Entities.ScriptResult", b => { b.Property("ID") .ValueGeneratedOnAdd() .HasColumnType("TEXT"); b.Property("DeviceID") + .IsRequired() .HasColumnType("TEXT"); b.Property("ErrorOutput") @@ -637,6 +614,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("INTEGER"); b.Property("OrganizationID") + .IsRequired() .HasColumnType("TEXT"); b.Property("RunTime") @@ -649,6 +627,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("INTEGER"); b.Property("ScriptInput") + .IsRequired() .HasColumnType("TEXT"); b.Property("ScriptRunId") @@ -676,6 +655,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("OrganizationID"); + b.HasIndex("SavedScriptId"); + b.HasIndex("ScheduleId"); b.HasIndex("ScriptRunId"); @@ -683,7 +664,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ScriptResults"); }); - modelBuilder.Entity("Remotely.Shared.Models.ScriptRun", b => + modelBuilder.Entity("Remotely.Shared.Entities.ScriptRun", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -696,6 +677,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("INTEGER"); b.Property("OrganizationID") + .IsRequired() .HasColumnType("TEXT"); b.Property("RunAt") @@ -711,19 +693,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ScheduleId") .HasColumnType("INTEGER"); - b.Property("ScriptScheduleId") - .HasColumnType("INTEGER"); - b.HasKey("Id"); b.HasIndex("OrganizationID"); - b.HasIndex("ScriptScheduleId"); + b.HasIndex("SavedScriptId"); + + b.HasIndex("ScheduleId"); b.ToTable("ScriptRuns"); }); - modelBuilder.Entity("Remotely.Shared.Models.ScriptSchedule", b => + modelBuilder.Entity("Remotely.Shared.Entities.ScriptSchedule", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -734,6 +715,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("CreatorId") + .IsRequired() .HasColumnType("TEXT"); b.Property("Interval") @@ -743,6 +725,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("Name") + .IsRequired() .HasColumnType("TEXT"); b.Property("NextRun") @@ -750,6 +733,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("OrganizationID") + .IsRequired() .HasColumnType("TEXT"); b.Property("RunOnNextConnect") @@ -771,7 +755,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ScriptSchedules"); }); - modelBuilder.Entity("Remotely.Shared.Models.SharedFile", b => + modelBuilder.Entity("Remotely.Shared.Entities.SharedFile", b => { b.Property("ID") .ValueGeneratedOnAdd() @@ -781,6 +765,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT"); b.Property("FileContents") + .IsRequired() .HasColumnType("BLOB"); b.Property("FileName") @@ -800,7 +785,29 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("SharedFiles"); }); - modelBuilder.Entity("Remotely.Shared.Models.RemotelyUser", b => + modelBuilder.Entity("Remotely.Shared.Models.DeviceGroup", b => + { + b.Property("ID") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationID") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("ID"); + + b.HasIndex("OrganizationID"); + + b.ToTable("DeviceGroups"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.RemotelyUser", b => { b.HasBaseType("Microsoft.AspNetCore.Identity.IdentityUser"); @@ -811,6 +818,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("INTEGER"); b.Property("OrganizationID") + .IsRequired() .HasColumnType("TEXT"); b.Property("TempPassword") @@ -834,7 +842,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Remotely.Shared.Models.RemotelyUser", null) + b.HasOne("Remotely.Shared.Entities.RemotelyUser", null) .WithMany() .HasForeignKey("UsersId") .OnDelete(DeleteBehavior.Cascade) @@ -849,7 +857,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Remotely.Shared.Models.ScriptSchedule", null) + b.HasOne("Remotely.Shared.Entities.ScriptSchedule", null) .WithMany() .HasForeignKey("ScriptSchedulesId") .OnDelete(DeleteBehavior.Cascade) @@ -858,43 +866,28 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("DeviceScriptRun", b => { - b.HasOne("Remotely.Shared.Models.Device", null) + b.HasOne("Remotely.Shared.Entities.Device", null) .WithMany() .HasForeignKey("DevicesID") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Remotely.Shared.Models.ScriptRun", null) + b.HasOne("Remotely.Shared.Entities.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) + b.HasOne("Remotely.Shared.Entities.Device", null) .WithMany() .HasForeignKey("DevicesID") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.HasOne("Remotely.Shared.Models.ScriptSchedule", null) + b.HasOne("Remotely.Shared.Entities.ScriptSchedule", null) .WithMany() .HasForeignKey("ScriptSchedulesId") .OnDelete(DeleteBehavior.Cascade) @@ -952,19 +945,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); - modelBuilder.Entity("Remotely.Shared.Models.Alert", b => + modelBuilder.Entity("Remotely.Shared.Entities.Alert", b => { - b.HasOne("Remotely.Shared.Models.Device", "Device") + b.HasOne("Remotely.Shared.Entities.Device", "Device") .WithMany("Alerts") - .HasForeignKey("DeviceID"); + .HasForeignKey("DeviceID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithMany("Alerts") - .HasForeignKey("OrganizationID"); + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Remotely.Shared.Models.RemotelyUser", "User") + b.HasOne("Remotely.Shared.Entities.RemotelyUser", "User") .WithMany("Alerts") - .HasForeignKey("UserID"); + .HasForeignKey("UserID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); b.Navigation("Device"); @@ -973,87 +972,98 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); - modelBuilder.Entity("Remotely.Shared.Models.ApiToken", b => + modelBuilder.Entity("Remotely.Shared.Entities.ApiToken", b => { - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithMany("ApiTokens") - .HasForeignKey("OrganizationID"); + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); b.Navigation("Organization"); }); - modelBuilder.Entity("Remotely.Shared.Models.BrandingInfo", b => + modelBuilder.Entity("Remotely.Shared.Entities.BrandingInfo", b => { - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithOne("BrandingInfo") - .HasForeignKey("Remotely.Shared.Models.BrandingInfo", "OrganizationId"); + .HasForeignKey("Remotely.Shared.Entities.BrandingInfo", "OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); b.Navigation("Organization"); }); - modelBuilder.Entity("Remotely.Shared.Models.Device", b => + modelBuilder.Entity("Remotely.Shared.Entities.Device", b => { b.HasOne("Remotely.Shared.Models.DeviceGroup", "DeviceGroup") .WithMany("Devices") .HasForeignKey("DeviceGroupID"); - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithMany("Devices") - .HasForeignKey("OrganizationID"); + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); 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 => + modelBuilder.Entity("Remotely.Shared.Entities.InviteLink", b => { - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithMany("InviteLinks") - .HasForeignKey("OrganizationID"); + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); b.Navigation("Organization"); }); - modelBuilder.Entity("Remotely.Shared.Models.SavedScript", b => + modelBuilder.Entity("Remotely.Shared.Entities.SavedScript", b => { - b.HasOne("Remotely.Shared.Models.RemotelyUser", "Creator") + b.HasOne("Remotely.Shared.Entities.RemotelyUser", "Creator") .WithMany("SavedScripts") - .HasForeignKey("CreatorId"); + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithMany("SavedScripts") - .HasForeignKey("OrganizationID"); + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); b.Navigation("Creator"); b.Navigation("Organization"); }); - modelBuilder.Entity("Remotely.Shared.Models.ScriptResult", b => + modelBuilder.Entity("Remotely.Shared.Entities.ScriptResult", b => { - b.HasOne("Remotely.Shared.Models.Device", "Device") + b.HasOne("Remotely.Shared.Entities.Device", "Device") .WithMany("ScriptResults") - .HasForeignKey("DeviceID"); + .HasForeignKey("DeviceID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithMany("ScriptResults") - .HasForeignKey("OrganizationID"); + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Remotely.Shared.Entities.SavedScript", "SavedScript") + .WithMany("ScriptResults") + .HasForeignKey("SavedScriptId"); - b.HasOne("Remotely.Shared.Models.ScriptSchedule", "Schedule") + b.HasOne("Remotely.Shared.Entities.ScriptSchedule", "Schedule") .WithMany() .HasForeignKey("ScheduleId"); - b.HasOne("Remotely.Shared.Models.ScriptRun", null) + b.HasOne("Remotely.Shared.Entities.ScriptRun", "ScriptRun") .WithMany("Results") .HasForeignKey("ScriptRunId"); @@ -1061,68 +1071,94 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Organization"); + b.Navigation("SavedScript"); + b.Navigation("Schedule"); + + b.Navigation("ScriptRun"); }); - modelBuilder.Entity("Remotely.Shared.Models.ScriptRun", b => + modelBuilder.Entity("Remotely.Shared.Entities.ScriptRun", b => { - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithMany("ScriptRuns") - .HasForeignKey("OrganizationID"); + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Remotely.Shared.Models.ScriptSchedule", null) + b.HasOne("Remotely.Shared.Entities.SavedScript", "SavedScript") .WithMany("ScriptRuns") - .HasForeignKey("ScriptScheduleId"); + .HasForeignKey("SavedScriptId"); + + b.HasOne("Remotely.Shared.Entities.ScriptSchedule", "Schedule") + .WithMany("ScriptRuns") + .HasForeignKey("ScheduleId"); b.Navigation("Organization"); + + b.Navigation("SavedScript"); + + b.Navigation("Schedule"); }); - modelBuilder.Entity("Remotely.Shared.Models.ScriptSchedule", b => + modelBuilder.Entity("Remotely.Shared.Entities.ScriptSchedule", b => { - b.HasOne("Remotely.Shared.Models.RemotelyUser", "Creator") + b.HasOne("Remotely.Shared.Entities.RemotelyUser", "Creator") .WithMany("ScriptSchedules") - .HasForeignKey("CreatorId"); + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithMany("ScriptSchedules") - .HasForeignKey("OrganizationID"); + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); b.Navigation("Creator"); b.Navigation("Organization"); }); - modelBuilder.Entity("Remotely.Shared.Models.SharedFile", b => + modelBuilder.Entity("Remotely.Shared.Entities.SharedFile", b => { - b.HasOne("Remotely.Shared.Models.Organization", "Organization") + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") .WithMany("SharedFiles") .HasForeignKey("OrganizationID"); b.Navigation("Organization"); }); - modelBuilder.Entity("Remotely.Shared.Models.RemotelyUser", b => + modelBuilder.Entity("Remotely.Shared.Models.DeviceGroup", b => { - b.HasOne("Remotely.Shared.Models.Organization", "Organization") - .WithMany("RemotelyUsers") - .HasForeignKey("OrganizationID"); + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("DeviceGroups") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); b.Navigation("Organization"); }); - modelBuilder.Entity("Remotely.Shared.Models.Device", b => + modelBuilder.Entity("Remotely.Shared.Entities.RemotelyUser", b => { - b.Navigation("Alerts"); + b.HasOne("Remotely.Shared.Entities.Organization", "Organization") + .WithMany("RemotelyUsers") + .HasForeignKey("OrganizationID") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); - b.Navigation("ScriptResults"); + b.Navigation("Organization"); }); - modelBuilder.Entity("Remotely.Shared.Models.DeviceGroup", b => + modelBuilder.Entity("Remotely.Shared.Entities.Device", b => { - b.Navigation("Devices"); + b.Navigation("Alerts"); + + b.Navigation("ScriptResults"); }); - modelBuilder.Entity("Remotely.Shared.Models.Organization", b => + modelBuilder.Entity("Remotely.Shared.Entities.Organization", b => { b.Navigation("Alerts"); @@ -1149,17 +1185,29 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("SharedFiles"); }); - modelBuilder.Entity("Remotely.Shared.Models.ScriptRun", b => + modelBuilder.Entity("Remotely.Shared.Entities.SavedScript", b => + { + b.Navigation("ScriptResults"); + + b.Navigation("ScriptRuns"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.ScriptRun", b => { b.Navigation("Results"); }); - modelBuilder.Entity("Remotely.Shared.Models.ScriptSchedule", b => + modelBuilder.Entity("Remotely.Shared.Entities.ScriptSchedule", b => { b.Navigation("ScriptRuns"); }); - modelBuilder.Entity("Remotely.Shared.Models.RemotelyUser", b => + modelBuilder.Entity("Remotely.Shared.Models.DeviceGroup", b => + { + b.Navigation("Devices"); + }); + + modelBuilder.Entity("Remotely.Shared.Entities.RemotelyUser", b => { b.Navigation("Alerts"); From 3d33fe12cc30f5204a67275c0426aaf86b7d9a3a Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Thu, 27 Jul 2023 12:24:17 -0700 Subject: [PATCH 23/29] Fix script output. --- Server/Pages/DeviceDetails.razor | 6 +++--- Server/Pages/DeviceDetails.razor.cs | 12 +++++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Server/Pages/DeviceDetails.razor b/Server/Pages/DeviceDetails.razor index c6a97118f..f8979bc2c 100644 --- a/Server/Pages/DeviceDetails.razor +++ b/Server/Pages/DeviceDetails.razor @@ -255,9 +255,9 @@ else @scriptResult.TimeStamp @scriptResult.SenderUserName @scriptResult.RunTime - @GetTrimmedText($"{scriptResult.ScriptInput}", 25) - @GetTrimmedText($"{scriptResult.StandardOutput}", 25) - @GetTrimmedText($"{scriptResult.ErrorOutput}", 25) + @GetTrimmedText(scriptResult.ScriptInput, 25) + @GetTrimmedText(scriptResult.StandardOutput, 25) + @GetTrimmedText(scriptResult.ErrorOutput, 25) diff --git a/Server/Pages/DeviceDetails.razor.cs b/Server/Pages/DeviceDetails.razor.cs index 3de0b5a5c..1e87451a8 100644 --- a/Server/Pages/DeviceDetails.razor.cs +++ b/Server/Pages/DeviceDetails.razor.cs @@ -156,7 +156,7 @@ private void GetScriptHistory() } } - private string GetTrimmedText(string source, int stringLength) + private string GetTrimmedText(string? source, int stringLength) { if (string.IsNullOrWhiteSpace(source)) { @@ -171,6 +171,12 @@ private string GetTrimmedText(string source, int stringLength) return source[0..25] + "..."; } + private string GetTrimmedText(string[]? source, int stringLength) + { + source ??= Array.Empty(); + return GetTrimmedText(string.Join("", source), stringLength); + } + private Task HandleValidSubmit() { if (_device is null) @@ -215,8 +221,8 @@ private void ShowFullScriptOutput(ScriptResult result) { void outputModal(RenderTreeBuilder builder) { - var output = string.Join("\r\n", $"{result.StandardOutput}"); - var error = string.Join("\r\n", $"{result.ErrorOutput}"); + var output = string.Join("\r\n", result.StandardOutput ?? Array.Empty()); + var error = string.Join("\r\n", result.ErrorOutput ?? Array.Empty()); var textareaStyle = "width: 100%; height: 200px; white-space: pre;"; builder.AddMarkupContent(0, "
Input
"); From 8104bfa5bac0098a9467b1997c63f58bd90c6082 Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Thu, 27 Jul 2023 14:22:21 -0700 Subject: [PATCH 24/29] Fix RemoteControlController. --- Server/API/AlertsController.cs | 28 ++++++++++++++++++++++++++- Server/API/RemoteControlController.cs | 20 ++++++++++++------- Server/Pages/ApiKeys.razor | 2 +- Shared/Models/AlertOptions.cs | 16 +++++++-------- 4 files changed, 49 insertions(+), 17 deletions(-) diff --git a/Server/API/AlertsController.cs b/Server/API/AlertsController.cs index 680b98b05..278a94d29 100644 --- a/Server/API/AlertsController.cs +++ b/Server/API/AlertsController.cs @@ -65,7 +65,20 @@ public async Task Create(AlertOptions alertOptions) { try { - await _emailSender.SendEmailAsync(alertOptions.EmailTo, + if (string.IsNullOrWhiteSpace(alertOptions.EmailTo)) + { + return BadRequest("Email address is required to send email."); + } + if (string.IsNullOrWhiteSpace(alertOptions.EmailSubject)) + { + return BadRequest("Email subject is required to send email."); + } + if (string.IsNullOrWhiteSpace(alertOptions.EmailBody)) + { + return BadRequest("Email body is required to send email."); + } + await _emailSender.SendEmailAsync( + alertOptions.EmailTo, alertOptions.EmailSubject, alertOptions.EmailBody, orgId.ToString()); @@ -81,6 +94,19 @@ await _emailSender.SendEmailAsync(alertOptions.EmailTo, { try { + if (string.IsNullOrWhiteSpace(alertOptions.ApiRequestUrl)) + { + return BadRequest("API request URL is required to send API request."); + } + if (string.IsNullOrWhiteSpace(alertOptions.ApiRequestMethod)) + { + return BadRequest("API request method is required to send API request."); + } + if (string.IsNullOrWhiteSpace(alertOptions.ApiRequestBody)) + { + return BadRequest("API request body is required to send API request."); + } + using var httpClient = _httpClientFactory.CreateClient(); using var request = new HttpRequestMessage( new HttpMethod(alertOptions.ApiRequestMethod), diff --git a/Server/API/RemoteControlController.cs b/Server/API/RemoteControlController.cs index a6ab6f992..4fc065aea 100644 --- a/Server/API/RemoteControlController.cs +++ b/Server/API/RemoteControlController.cs @@ -68,6 +68,7 @@ public async Task Get(string deviceID) } [HttpPost] + [Obsolete("This method is deprecated. Use the GET method along with API keys instead.")] public async Task Post([FromBody] RemoteControlRequest rcRequest) { if (!_appConfig.AllowApiLogin) @@ -111,7 +112,7 @@ public async Task Post([FromBody] RemoteControlRequest rcRequest) return BadRequest(); } - private async Task InitiateRemoteControl(string deviceID, string orgID) + private async Task InitiateRemoteControl(string deviceID, string orgId) { if (!_serviceSessionCache.TryGetByDeviceId(deviceID, out var targetDevice) || !_serviceSessionCache.TryGetConnectionId(deviceID, out var serviceConnectionId)) @@ -119,7 +120,7 @@ private async Task InitiateRemoteControl(string deviceID, string return NotFound("The target device couldn't be found."); } - if (targetDevice.OrganizationID != orgID) + if (targetDevice.OrganizationID != orgId) { return Unauthorized(); } @@ -141,7 +142,7 @@ private async Task InitiateRemoteControl(string deviceID, string var sessionCount = _remoteControlSessionCache.Sessions .OfType() - .Count(x => x.OrganizationId == orgID); + .Count(x => x.OrganizationId == orgId); if (sessionCount > _appConfig.RemoteControlSessionLimit) { @@ -157,7 +158,7 @@ private async Task InitiateRemoteControl(string deviceID, string UserConnectionId = HttpContext.Connection.Id, AgentConnectionId = serviceConnectionId, DeviceId = deviceID, - OrganizationId = orgID + OrganizationId = orgId }; _remoteControlSessionCache.AddOrUpdate($"{sessionId}", session, (k, v) => @@ -171,15 +172,20 @@ private async Task InitiateRemoteControl(string deviceID, string return session; }); - var orgName = _dataService.GetOrganizationNameById(orgID); + var orgNameResult = await _dataService.GetOrganizationNameById(orgId); + + if (!orgNameResult.IsSuccess) + { + return BadRequest("Failed to resolve organization name."); + } await _serviceHub.Clients.Client(serviceConnectionId).SendAsync("RemoteControl", sessionId, accessKey, HttpContext.Connection.Id, string.Empty, - orgName, - orgID); + orgNameResult.Value, + orgId); var waitResult = await session.WaitForSessionReady(TimeSpan.FromSeconds(30)); if (!waitResult) diff --git a/Server/Pages/ApiKeys.razor b/Server/Pages/ApiKeys.razor index f773328db..31d14f316 100644 --- a/Server/Pages/ApiKeys.razor +++ b/Server/Pages/ApiKeys.razor @@ -91,7 +91,7 @@ else private async Task CreateNewKey() { - var secret = RandomGenerator.GenerateString(36); + var secret = RandomGenerator.GenerateString(48); var secretHash = new PasswordHasher().HashPassword(string.Empty, secret); var result = await DataService.CreateApiToken(UserName, _createKeyName, secretHash); diff --git a/Shared/Models/AlertOptions.cs b/Shared/Models/AlertOptions.cs index 1ffda6ec7..25e2f54e3 100644 --- a/Shared/Models/AlertOptions.cs +++ b/Shared/Models/AlertOptions.cs @@ -4,15 +4,15 @@ namespace Remotely.Shared.Models; public class AlertOptions { - public string AlertDeviceID { get; set; } = string.Empty; - public string AlertMessage { get; set; } = string.Empty; - public string ApiRequestBody { get; set; } = string.Empty; + public required string AlertDeviceID { get; set; } + public required string AlertMessage { get; set; } + public string? ApiRequestBody { get; set; } public Dictionary ApiRequestHeaders { get; set; } = new(); - public string ApiRequestMethod { get; set; } = string.Empty; - public string ApiRequestUrl { get; set; } = string.Empty; - public string EmailBody { get; set; } = string.Empty; - public string EmailSubject { get; set; } = string.Empty; - public string EmailTo { get; set; } = string.Empty; + public string? ApiRequestMethod { get; set; } + public string? ApiRequestUrl { get; set; } + public string? EmailBody { get; set; } + public string? EmailSubject { get; set; } + public string? EmailTo { get; set; } public bool ShouldAlert { get; set; } public bool ShouldEmail { get; set; } public bool ShouldSendApiRequest { get; set; } From b2609fdbb744de594bbe1d14cf86f0371fdb7459 Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Thu, 27 Jul 2023 14:47:46 -0700 Subject: [PATCH 25/29] Add method back for backwards compatibility. --- Server/API/AgentUpdateController.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Server/API/AgentUpdateController.cs b/Server/API/AgentUpdateController.cs index 215c1e462..9bd6900cb 100644 --- a/Server/API/AgentUpdateController.cs +++ b/Server/API/AgentUpdateController.cs @@ -37,7 +37,6 @@ public AgentUpdateController(IWebHostEnvironment hostingEnv, _logger = logger; } - [HttpGet("[action]/{platform}")] [EnableRateLimiting(PolicyNames.AgentUpdateDownloads)] public async Task DownloadPackage(string platform) @@ -89,6 +88,15 @@ public async Task DownloadPackage(string platform) } } + + [HttpGet("[action]/{platform}/{downloadId}")] + [EnableRateLimiting(PolicyNames.AgentUpdateDownloads)] + [Obsolete("This method is only for backwards compatibility. Remove after a few releases.")] + public async Task DownloadPackage(string platform, string downloadId) + { + return await DownloadPackage(platform); + } + private async Task CheckForDeviceBan(string deviceIp) { if (string.IsNullOrWhiteSpace(deviceIp)) From 88ed5f8d6acdd858b87d0d981183becdedbe8cd5 Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Thu, 27 Jul 2023 14:54:00 -0700 Subject: [PATCH 26/29] Fix call to get org name. --- Server/Hubs/CircuitConnection.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Server/Hubs/CircuitConnection.cs b/Server/Hubs/CircuitConnection.cs index b37b564dc..e9e4886b3 100644 --- a/Server/Hubs/CircuitConnection.cs +++ b/Server/Hubs/CircuitConnection.cs @@ -326,13 +326,12 @@ public async Task> RemoteControl(string deviceId, return Result.Fail(orgResult.Reason); } - var organization = _dataService.GetOrganizationNameByUserName($"{User.UserName}"); await _agentHubContext.Clients.Client(serviceConnectionId).SendAsync("RemoteControl", sessionId, accessKey, ConnectionId, User.UserOptions?.DisplayName, - organization, + orgResult.Value, User.OrganizationID); return Result.Ok(session); From f10ed874ffc4c36cadf4fe770626c0a7da65a705 Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Fri, 28 Jul 2023 14:34:30 -0700 Subject: [PATCH 27/29] Add more cleanup to migration. --- .../20230726225818_Enable_NullableReferences.cs | 9 ++++++++- .../20230726225809_Enable_NullableReferences.cs | 9 ++++++++- .../20230726225800_Enable_NullableReferences.cs | 15 ++++++++++++--- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/Server/Migrations/PostgreSql/20230726225818_Enable_NullableReferences.cs b/Server/Migrations/PostgreSql/20230726225818_Enable_NullableReferences.cs index 1f7175a92..3ca45de4e 100644 --- a/Server/Migrations/PostgreSql/20230726225818_Enable_NullableReferences.cs +++ b/Server/Migrations/PostgreSql/20230726225818_Enable_NullableReferences.cs @@ -10,8 +10,15 @@ public partial class Enable_NullableReferences : Migration /// protected override void Up(MigrationBuilder migrationBuilder) { - // This relationship wasn't enforced previously. + // These relationships weren't enforced previously. migrationBuilder.Sql("delete from ScriptResults where DeviceID is null;"); + migrationBuilder.Sql("delete from ScriptResults where DeviceID not in (select ID from Devices);"); + migrationBuilder.Sql("delete from ScriptResults where OrganizationID is null;"); + migrationBuilder.Sql("delete from ScriptResults where OrganizationID not in (select ID from Organizations);"); + migrationBuilder.Sql("delete from ScriptResults where SavedScriptId not in (select Id from SavedScripts);"); + migrationBuilder.Sql("delete from ScriptRuns where OrganizationID is null;"); + migrationBuilder.Sql("delete from ScriptRuns where OrganizationID not in (select ID from Organizations);"); + migrationBuilder.Sql("delete from ScriptRuns where SavedScriptId not in (select Id from SavedScripts);"); migrationBuilder.DropForeignKey( name: "FK_Alerts_Devices_DeviceID", diff --git a/Server/Migrations/SqlServer/20230726225809_Enable_NullableReferences.cs b/Server/Migrations/SqlServer/20230726225809_Enable_NullableReferences.cs index 6adcd6556..7df579f28 100644 --- a/Server/Migrations/SqlServer/20230726225809_Enable_NullableReferences.cs +++ b/Server/Migrations/SqlServer/20230726225809_Enable_NullableReferences.cs @@ -10,8 +10,15 @@ public partial class Enable_NullableReferences : Migration /// protected override void Up(MigrationBuilder migrationBuilder) { - // This relationship wasn't enforced previously. + // These relationships weren't enforced previously. migrationBuilder.Sql("delete from ScriptResults where DeviceID is null;"); + migrationBuilder.Sql("delete from ScriptResults where DeviceID not in (select ID from Devices);"); + migrationBuilder.Sql("delete from ScriptResults where OrganizationID is null;"); + migrationBuilder.Sql("delete from ScriptResults where OrganizationID not in (select ID from Organizations);"); + migrationBuilder.Sql("delete from ScriptResults where SavedScriptId not in (select Id from SavedScripts);"); + migrationBuilder.Sql("delete from ScriptRuns where OrganizationID is null;"); + migrationBuilder.Sql("delete from ScriptRuns where OrganizationID not in (select ID from Organizations);"); + migrationBuilder.Sql("delete from ScriptRuns where SavedScriptId not in (select Id from SavedScripts);"); migrationBuilder.DropForeignKey( name: "FK_Alerts_Devices_DeviceID", diff --git a/Server/Migrations/Sqlite/20230726225800_Enable_NullableReferences.cs b/Server/Migrations/Sqlite/20230726225800_Enable_NullableReferences.cs index b5103639c..6b33b3318 100644 --- a/Server/Migrations/Sqlite/20230726225800_Enable_NullableReferences.cs +++ b/Server/Migrations/Sqlite/20230726225800_Enable_NullableReferences.cs @@ -1,4 +1,6 @@ using Microsoft.EntityFrameworkCore.Migrations; +using Remotely.Server.Components.Scripts; +using Remotely.Shared.Entities; #nullable disable @@ -10,12 +12,19 @@ public partial class Enable_NullableReferences : Migration /// protected override void Up(MigrationBuilder migrationBuilder) { - // This relationship wasn't enforced previously. + // These relationships weren't enforced previously. migrationBuilder.Sql("delete from ScriptResults where DeviceID is null;"); + migrationBuilder.Sql("delete from ScriptResults where DeviceID not in (select ID from Devices);"); + migrationBuilder.Sql("delete from ScriptResults where OrganizationID is null;"); + migrationBuilder.Sql("delete from ScriptResults where OrganizationID not in (select ID from Organizations);"); + migrationBuilder.Sql("delete from ScriptResults where SavedScriptId not in (select Id from SavedScripts);"); + migrationBuilder.Sql("delete from ScriptRuns where OrganizationID is null;"); + migrationBuilder.Sql("delete from ScriptRuns where OrganizationID not in (select ID from Organizations);"); + migrationBuilder.Sql("delete from ScriptRuns where SavedScriptId not in (select Id from SavedScripts);"); migrationBuilder.DropForeignKey( - name: "FK_Alerts_Devices_DeviceID", - table: "Alerts"); + name: "FK_Alerts_Devices_DeviceID", + table: "Alerts"); migrationBuilder.DropForeignKey( name: "FK_Alerts_Organizations_OrganizationID", From 336610cc08beb175495e74aa0a88a566426c7a34 Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Mon, 31 Jul 2023 08:16:41 -0700 Subject: [PATCH 28/29] Update Immense.RemoteControl --- submodules/Immense.RemoteControl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/Immense.RemoteControl b/submodules/Immense.RemoteControl index 0c08b0028..d571422a0 160000 --- a/submodules/Immense.RemoteControl +++ b/submodules/Immense.RemoteControl @@ -1 +1 @@ -Subproject commit 0c08b00282ea01d358ad51a20693e8ec4fca2071 +Subproject commit d571422a0e282fd009769bf14c7f6adf8867b0d8 From 7a362c9cb9a72f7857c3ed5ac99a6b72f799ee7a Mon Sep 17 00:00:00 2001 From: Jared Goodwin Date: Mon, 31 Jul 2023 11:16:50 -0700 Subject: [PATCH 29/29] Add cancellation token to FileLogsManager methods. --- Agent/Services/AgentHubConnection.cs | 10 ++++++--- Agent/Services/FileLogsManager.cs | 33 ++++++++++++++++++++-------- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/Agent/Services/AgentHubConnection.cs b/Agent/Services/AgentHubConnection.cs index 2e759d1d3..0aad376ca 100644 --- a/Agent/Services/AgentHubConnection.cs +++ b/Agent/Services/AgentHubConnection.cs @@ -1,6 +1,7 @@ using Immense.RemoteControl.Desktop.Native.Windows; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Remotely.Agent.Extensions; using Remotely.Agent.Interfaces; @@ -40,6 +41,7 @@ public class AgentHubConnection : IAgentHubConnection, IDisposable private readonly IWakeOnLanService _wakeOnLanService; private readonly ILogger _logger; private readonly IFileLogsManager _fileLogsManager; + private readonly IHostApplicationLifetime _appLifetime; private readonly IScriptExecutor _scriptExecutor; private readonly IUninstaller _uninstaller; private readonly IUpdater _updater; @@ -60,6 +62,7 @@ public AgentHubConnection( IHttpClientFactory httpFactory, IWakeOnLanService wakeOnLanService, IFileLogsManager fileLogsManager, + IHostApplicationLifetime appLifetime, ILogger logger) { _configService = configService; @@ -73,6 +76,7 @@ public AgentHubConnection( _wakeOnLanService = wakeOnLanService; _logger = logger; _fileLogsManager = fileLogsManager; + _appLifetime = appLifetime; } public bool IsConnected => _hubConnection?.State == HubConnectionState.Connected; @@ -302,7 +306,7 @@ private void RegisterMessageHandlers() _hubConnection.On("DeleteLogs", () => { - _fileLogsManager.DeleteLogs(); + _fileLogsManager.DeleteLogs(_appLifetime.ApplicationStopping); }); @@ -367,14 +371,14 @@ private void RegisterMessageHandlers() { try { - if (!await _fileLogsManager.AnyLogsExist()) + if (!await _fileLogsManager.AnyLogsExist(_appLifetime.ApplicationStopping)) { var message = "There are no log entries written."; await _hubConnection.InvokeAsync("SendLogs", message, senderConnectionId).ConfigureAwait(false); return; } - await foreach (var chunk in _fileLogsManager.ReadAllBytes()) + await foreach (var chunk in _fileLogsManager.ReadAllBytes(_appLifetime.ApplicationStopping)) { var lines = Encoding.UTF8.GetString(chunk); await _hubConnection.InvokeAsync("SendLogs", lines, senderConnectionId).ConfigureAwait(false); diff --git a/Agent/Services/FileLogsManager.cs b/Agent/Services/FileLogsManager.cs index a9684b631..665ad7d88 100644 --- a/Agent/Services/FileLogsManager.cs +++ b/Agent/Services/FileLogsManager.cs @@ -4,23 +4,25 @@ using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace Remotely.Shared.Services; public interface IFileLogsManager { - Task AnyLogsExist(); - Task DeleteLogs(); - IAsyncEnumerable ReadAllBytes(); + Task AnyLogsExist(CancellationToken cancellationToken); + Task DeleteLogs(CancellationToken cancellationToken); + IAsyncEnumerable ReadAllBytes(CancellationToken cancellationToken); } public class FileLogsManager : IFileLogsManager { - public async Task AnyLogsExist() + public async Task AnyLogsExist(CancellationToken cancellationToken) { - using var logLock = await FileLoggerDefaults.AcquireLock(); + using var logLock = await FileLoggerDefaults.AcquireLock(cancellationToken); var componentName = Assembly.GetExecutingAssembly().GetName().Name; var directory = Path.Combine(FileLoggerDefaults.LogsFolderPath, $"{componentName}"); @@ -29,6 +31,11 @@ public async Task AnyLogsExist() { foreach (var file in Directory.GetFiles(directory)) { + if (cancellationToken.IsCancellationRequested) + { + break; + } + if (new FileInfo(file).Length > 0) { return true; @@ -38,9 +45,9 @@ public async Task AnyLogsExist() return false; } - public async Task DeleteLogs() + public async Task DeleteLogs(CancellationToken cancellationToken) { - using var logLock = await FileLoggerDefaults.AcquireLock(); + using var logLock = await FileLoggerDefaults.AcquireLock(cancellationToken); var componentName = Assembly.GetExecutingAssembly().GetName().Name; var directory = Path.Combine(FileLoggerDefaults.LogsFolderPath, $"{componentName}"); @@ -49,6 +56,10 @@ public async Task DeleteLogs() { foreach (var file in Directory.GetFiles(directory)) { + if (cancellationToken.IsCancellationRequested) + { + break; + } try { File.Delete(file); @@ -58,9 +69,9 @@ public async Task DeleteLogs() } } - public async IAsyncEnumerable ReadAllBytes() + public async IAsyncEnumerable ReadAllBytes([EnumeratorCancellation] CancellationToken cancellationToken) { - using var logLock = await FileLoggerDefaults.AcquireLock(); + using var logLock = await FileLoggerDefaults.AcquireLock(cancellationToken); var componentName = Assembly.GetExecutingAssembly().GetName().Name; var directory = Path.Combine(FileLoggerDefaults.LogsFolderPath, $"{componentName}"); @@ -78,6 +89,10 @@ public async IAsyncEnumerable ReadAllBytes() { foreach (var chunk in File.ReadAllBytes(file).Chunk(50_000)) { + if (cancellationToken.IsCancellationRequested) + { + yield break; + } yield return File.ReadAllBytes(file); } }