Refactor DataService for nullability. Need to refactor calling services next.

This commit is contained in:
Jared Goodwin 2023-07-24 16:11:43 -07:00
parent e5470f9d5f
commit b500f7e9e0
23 changed files with 901 additions and 539 deletions

View File

@ -107,7 +107,30 @@ public class ScriptExecutor : IScriptExecutor
var url = $"{connectionInfo.Host}/API/SavedScripts/{savedScriptId}";
using var hc = new HttpClient();
hc.DefaultRequestHeaders.Add("Authorization", authToken);
var savedScript = await hc.GetFromJsonAsync<SavedScript>(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<SavedScript>();
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(),

6
Directory.Build.props Normal file
View File

@ -0,0 +1,6 @@
<Project>
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -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

View File

@ -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 class AlertsController : ControllerBase
[HttpPost("Create")]
public async Task<IActionResult> 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 class AlertsController : ControllerBase
{
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 @@ public class AlertsController : ControllerBase
[HttpDelete("Delete/{alertID}")]
public async Task<IActionResult> 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 class AlertsController : ControllerBase
[HttpDelete("DeleteAll")]
public async Task<IActionResult> 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)
{

View File

@ -6,6 +6,7 @@ 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;
@ -117,8 +118,11 @@ public class ClientDownloadsController : ControllerBase
[HttpGet("{platformID}")]
public async Task<IActionResult> 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}")]

View File

@ -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 class DevicesController : ControllerBase
[ServiceFilter(typeof(ApiAuthorizationFilter))]
public IEnumerable<Device> Get()
{
Request.Headers.TryGetValue("OrganizationID", out var orgID);
if (!Request.Headers.TryGetOrganizationId(out var orgId))
{
return Array.Empty<Device>();
}
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<Device> 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 class DevicesController : ControllerBase
[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();
}

View File

@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc;
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 class OrganizationManagementController : ControllerBase
[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 class OrganizationManagementController : ControllerBase
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 class OrganizationManagementController : ControllerBase
Request.Headers.TryGetValue("OrganizationID", out var orgID);
await DataService.DeleteUser(orgID, userID);
return Ok("ok");
return NoContent();
}
[HttpGet("DeviceGroup")]
@ -117,7 +133,7 @@ public class OrganizationManagementController : ControllerBase
Request.Headers.TryGetValue("OrganizationID", out var orgID);
DataService.DeleteDeviceGroup(orgID, deviceGroupID.Trim());
return Ok("ok");
return NoContent();
}
[HttpPost("DeviceGroup")]
@ -240,7 +256,7 @@ public class OrganizationManagementController : ControllerBase
Request.Headers.TryGetValue("OrganizationID", out var orgID);
DataService.UpdateOrganizationName(orgID, organizationName.Trim());
return Ok("ok");
return NoContent();
}
[HttpPut("SetDefault")]
@ -255,7 +271,7 @@ public class OrganizationManagementController : ControllerBase
Request.Headers.TryGetValue("OrganizationID", out var orgID);
DataService.SetIsDefaultOrganization(orgID, isDefault);
return Ok("ok");
return NoContent();
}
[HttpPost("SendInvite")]
@ -272,30 +288,41 @@ public class OrganizationManagementController : ControllerBase
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 user = await UserManager.FindByEmailAsync(invite.InvitedUser);
await UserManager.ConfirmEmailAsync(user, await UserManager.GenerateEmailConfirmationTokenAsync(user));
return Ok();
}
else
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);
if (user is null)
{
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);
var inviteURL = $"{Request.Scheme}://{Request.Host}/Invite?id={newInvite.ID}";
if (!newInvite.IsSuccess)
{
return BadRequest(newInvite.Reason);
}
var inviteURL = $"{Request.Scheme}://{Request.Host}/Invite?id={newInvite.Value.ID}";
var emailResult = await EmailSender.SendEmailAsync(invite.InvitedUser, "Invitation to Organization in Remotely",
$@"<img src='{Request.Scheme}://{Request.Host}/images/Remotely_Logo.png'/>
<br><br>
@ -304,7 +331,7 @@ public class OrganizationManagementController : ControllerBase
You've been invited to join an organization in Remotely.
<br><br>
You can join the organization by <a href='{HtmlEncoder.Default.Encode(inviteURL)}'>clicking here</a>.",
orgID);
orgId);
if (!emailResult)
{

View File

@ -45,39 +45,40 @@ public class RegisterModel : PageModel
}
[BindProperty]
public InputModel Input { get; set; }
public int OrganizationCount { get; set; }
public string ReturnUrl { get; set; }
public InputModel Input { get; set; } = null!;
public IList<AuthenticationScheme> ExternalLogins { get; set; }
public int OrganizationCount { get; set; }
public string? ReturnUrl { get; set; }
public IList<AuthenticationScheme>? 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<IActionResult> OnPostAsync(string returnUrl = null)
public async Task<IActionResult> OnPostAsync(string? returnUrl = null)
{
var organizationCount = _dataService.GetOrganizationCount();
if (_appConfig.MaxOrganizationCount > 0 && organizationCount >= _appConfig.MaxOrganizationCount)
@ -110,15 +111,20 @@ public class RegisterModel : PageModel
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 <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
if (_userManager.Options.SignIn.RequireConfirmedAccount)
{
return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl });
return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl });
}
else
{

View File

@ -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 @@ public partial class ScriptSchedules : AuthComponentBase
{
_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();
}

View File

@ -18,7 +18,7 @@ namespace Remotely.Server.Data;
public class AppDb : IdentityDbContext
{
private static readonly ValueComparer<string[]> _stringArrayComparer = new(
(a, b) => a.SequenceEqual(b),
(a, b) => (a ?? Array.Empty<string>()).SequenceEqual(b ?? Array.Empty<string>()),
c => c.Aggregate(0, (a, b) => HashCode.Combine(a, b.GetHashCode())),
c => c.ToArray());
@ -131,9 +131,6 @@ public class AppDb : IdentityDbContext
builder.Entity<Device>()
.HasMany(x => x.ScriptRuns)
.WithMany(x => x.Devices);
builder.Entity<Device>()
.HasMany(x => x.ScriptRunsCompleted)
.WithMany(x => x.DevicesCompleted);
builder.Entity<Device>()
.HasMany(x => x.ScriptSchedules)
.WithMany(x => x.Devices);
@ -153,9 +150,6 @@ public class AppDb : IdentityDbContext
builder.Entity<ScriptRun>()
.HasMany(x => x.Devices)
.WithMany(x => x.ScriptRuns);
builder.Entity<ScriptRun>()
.HasMany(x => x.DevicesCompleted)
.WithMany(x => x.ScriptRunsCompleted);
builder.Entity<ScriptResult>()
.Property(x => x.ErrorOutput)

View File

@ -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;
}
}

View File

@ -24,30 +24,30 @@ public partial class ManageOrganization : AuthComponentBase
private readonly List<InviteLink> _invites = new();
private readonly List<RemotelyUser> _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<RemotelyUser> UserManager { get; set; }
private UserManager<RemotelyUser> UserManager { get; set; } = null!;
protected override async Task OnInitializedAsync()
@ -88,12 +88,21 @@ public partial class ManageOrganization : AuthComponentBase
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 @@ public partial class ManageOrganization : AuthComponentBase
}
private void OrganizationNameChanged(ChangeEventArgs args)
{
if (_organization is null)
{
return;
}
if (!User.IsAdministrator)
{
return;
@ -290,9 +304,15 @@ public partial class ManageOrganization : AuthComponentBase
InvitedUser = _inviteEmail,
IsAdmin = _inviteAsAdmin
};
var newInvite = DataService.AddInvite(User.OrganizationID, invite);
var newInvite = await DataService.AddInvite(User.OrganizationID, invite);
var inviteURL = $"{NavManager.BaseUri}Invite?id={newInvite.ID}";
if (!newInvite.IsSuccess)
{
ToastService.ShowToast($"Failed to create invite. {newInvite.Reason}", classString: "bg-danger");
return;
}
var inviteURL = $"{NavManager.BaseUri}Invite?id={newInvite.Value.ID}";
var emailResult = await EmailSender.SendEmailAsync(invite.InvitedUser, "Invitation to Organization in Remotely",
$@"<img src='{NavManager.BaseUri}images/Remotely_Logo.png'/>
<br><br>
@ -308,7 +328,7 @@ public partial class ManageOrganization : AuthComponentBase
_inviteAsAdmin = false;
_inviteEmail = string.Empty;
_invites.Add(newInvite);
_invites.Add(newInvite.Value);
}
else
{
@ -324,7 +344,11 @@ public partial class ManageOrganization : AuthComponentBase
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.");
}

File diff suppressed because it is too large Load Diff

View File

@ -76,17 +76,15 @@ public class Device
public int ProcessorCount { get; set; }
public string? PublicIP { get; set; }
[JsonIgnore]
public List<ScriptResult>? ScriptResults { get; set; }
[JsonIgnore]
public List<ScriptRun>? ScriptRuns { get; set; }
public List<ScriptResult> ScriptResults { get; set; } = new();
[JsonIgnore]
public List<ScriptRun>? ScriptRunsCompleted { get; set; }
public List<ScriptRun> ScriptRuns { get; set; } = new();
[JsonIgnore]
public List<ScriptSchedule>? ScriptSchedules { get; set; }
public List<ScriptSchedule> ScriptSchedules { get; set; } = new();
public string? ServerVerificationToken { get; set; }

View File

@ -16,7 +16,7 @@ public class DeviceGroup
public string ID { get; set; } = null!;
[JsonIgnore]
public List<Device>? Devices { get; set; }
public List<Device> 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<RemotelyUser>? Users { get; set; }
public List<RemotelyUser> Users { get; set; } = new();
[JsonIgnore]
public List<ScriptSchedule>? ScriptSchedules { get; set; }

View File

@ -9,34 +9,34 @@ namespace Remotely.Shared.Models;
public class Organization
{
public ICollection<Alert>? Alerts { get; set; }
public ICollection<Alert> Alerts { get; set; } = new List<Alert>();
public ICollection<ApiToken>? ApiTokens { get; set; }
public ICollection<ApiToken> ApiTokens { get; set; } = new List<ApiToken>();
public BrandingInfo? BrandingInfo { get; set; }
public ICollection<ScriptResult>? ScriptResults { get; set; }
public ICollection<ScriptResult> ScriptResults { get; set; } = new List<ScriptResult>();
public ICollection<ScriptRun>? ScriptRuns { get; set; }
public ICollection<SavedScript>? SavedScripts { get; set; }
public ICollection<ScriptRun> ScriptRuns { get; set; } = new List<ScriptRun>();
public ICollection<SavedScript> SavedScripts { get; set; } = new List<SavedScript>();
public ICollection<ScriptSchedule>? ScriptSchedules { get; set; }
public ICollection<ScriptSchedule> ScriptSchedules { get; set; } = new List<ScriptSchedule>();
public ICollection<DeviceGroup>? DeviceGroups { get; set; }
public ICollection<DeviceGroup> DeviceGroups { get; set; } = new List<DeviceGroup>();
public ICollection<Device>? Devices { get; set; }
public ICollection<Device> Devices { get; set; } = new List<Device>();
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public string ID { get; set; } = null!;
public ICollection<InviteLink>? InviteLinks { get; set; }
public ICollection<InviteLink> InviteLinks { get; set; } = new List<InviteLink>();
public bool IsDefaultOrganization { get; set; }
[StringLength(25)]
public required string OrganizationName { get; set; }
public ICollection<RemotelyUser>? RemotelyUsers { get; set; }
public ICollection<SharedFile>? SharedFiles { get; set; }
public ICollection<RemotelyUser> RemotelyUsers { get; set; } = new List<RemotelyUser>();
public ICollection<SharedFile> SharedFiles { get; set; } = new List<SharedFile>();
}

View File

@ -7,9 +7,9 @@ namespace Remotely.Shared.Models;
public class RemotelyUser : IdentityUser
{
public ICollection<Alert>? Alerts { get; set; }
public ICollection<Alert> Alerts { get; set; } = new List<Alert>();
public List<DeviceGroup>? DeviceGroups { get; set; }
public List<DeviceGroup> 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<SavedScript>? SavedScripts { get; set; }
public List<ScriptSchedule>? ScriptSchedules { get; set; }
public List<SavedScript> SavedScripts { get; set; } = new();
public List<ScriptSchedule> ScriptSchedules { get; set; } = new();
public string? TempPassword { get; set; }

View File

@ -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; }

View File

@ -13,23 +13,22 @@ namespace Remotely.Shared.Models;
public class ScriptRun
{
[JsonIgnore]
public List<Device> Devices { get; set; }
[JsonIgnore]
public List<Device> DevicesCompleted { get; set; }
public List<Device>? 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<ScriptResult> Results { get; set; }
public List<ScriptResult>? Results { get; set; }
public DateTimeOffset RunAt { get; set; }
public bool RunOnNextConnect { get; set; }

View File

@ -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<DeviceGroup> DeviceGroups { get; set; }
public List<DeviceGroup> DeviceGroups { get; set; } = new();
[JsonIgnore]
public List<Device> Devices { get; set; }
public List<Device> 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<ScriptRun> ScriptRuns { get; set; }
public List<ScriptRun> ScriptRuns { get; set; } = new();
}

View File

@ -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<byte>();
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!;
}

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<AssemblyName>Remotely_Shared</AssemblyName>
<AssemblyName></AssemblyName>
<Platforms>AnyCPU;x64;x86</Platforms>
<RootNamespace>Remotely.Shared</RootNamespace>
<Nullable>enable</Nullable>

View File

@ -159,8 +159,8 @@ public class DataServiceTests
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 class DataServiceTests
pendingRuns = await _dataService.GetPendingScriptRuns(_testData.Org1Device1.ID);
Assert.AreEqual(0, pendingRuns.Count);
Assert.AreEqual(0, pendingRuns.Count());
}