From 78204e88cb6770a2d655f3f772fdc8d0a5884355 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Thu, 13 Feb 2025 14:39:17 +0100 Subject: [PATCH 1/9] Decrease IEnumerable usage This will reduce the risk of multiple enumeration crashes and probably improve performance --- API/Controller/Admin/GetOnlineDevices.cs | 37 +++++----- API/Controller/Sessions/ListSessions.cs | 6 +- API/Controller/Shares/V2GetShares.cs | 2 +- API/Controller/Shares/V2Requests.cs | 4 +- .../Shockers/ControlLogController.cs | 2 +- .../Shockers/ControlShockerController.cs | 2 +- .../Shockers/OwnShockerController.cs | 23 +++--- .../Shockers/ShareShockerController.cs | 4 +- .../Shockers/SharedShockersController.cs | 73 +++++++++++-------- API/Controller/Tokens/TokenController.cs | 6 +- API/Models/Requests/CreateShareRequest.cs | 2 +- API/Models/Response/DeviceSelfResponse.cs | 2 +- API/Models/Response/OwnerShockerResponse.cs | 4 +- .../Response/ResponseDeviceWithShockers.cs | 2 +- API/Models/Response/UserSharesResponse.cs | 2 +- API/Services/Email/Mailjet/Mail/MailBase.cs | 2 +- API/Services/Email/Mailjet/Mail/MailsWrap.cs | 2 +- .../Email/Mailjet/MailjetEmailService.cs | 4 +- ...ockAuthorizationMiddlewareResultHandler.cs | 2 +- Common/Errors/AuthorizationError.cs | 4 +- Common/Errors/ShareError.cs | 2 +- Common/Models/ControlRequest.cs | 2 +- Common/Models/Paginated.cs | 2 +- Common/Models/PermissionType.cs | 26 ++++--- .../CustomProblems/PolicyNotMetProblem.cs | 4 +- .../CustomProblems/ShockersNotFoundProblem.cs | 4 +- .../CustomProblems/TokenPermissionProblem.cs | 4 +- Common/Services/Session/ISessionService.cs | 2 +- Common/Services/Session/SessionService.cs | 7 +- Common/Utils/TrustedProxiesFetcher.cs | 7 +- Common/Websocket/WebsocketCollection.cs | 6 +- 31 files changed, 136 insertions(+), 115 deletions(-) diff --git a/API/Controller/Admin/GetOnlineDevices.cs b/API/Controller/Admin/GetOnlineDevices.cs index 7fe4afaa..d231d4b9 100644 --- a/API/Controller/Admin/GetOnlineDevices.cs +++ b/API/Controller/Admin/GetOnlineDevices.cs @@ -20,7 +20,7 @@ public sealed partial class AdminController /// All online devices /// Unauthorized [HttpGet("monitoring/onlineDevices")] - [ProducesResponseType>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] + [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] public async Task GetOnlineDevices() { var devicesOnline = _redis.RedisCollection(false); @@ -41,23 +41,26 @@ public async Task GetOnlineDevices() } }).ToArrayAsync(); - return RespondSuccessLegacy(allOnlineDevices.Select(x => - { - var dbItem = dbLookup.First(y => y.Id == x.Id); - return new AdminOnlineDeviceResponse + return RespondSuccessLegacy( + allOnlineDevices + .Select(x => { - Id = x.Id, - FirmwareVersion = x.FirmwareVersion, - Gateway = x.Gateway, - Owner = dbItem.Owner, - Name = dbItem.Name, - ConnectedAt = x.ConnectedAt, - UserAgent = x.UserAgent, - BootedAt = x.BootedAt, - LatencyMs = x.LatencyMs, - Rssi = x.Rssi, - }; - }) + var dbItem = dbLookup.First(y => y.Id == x.Id); + return new AdminOnlineDeviceResponse + { + Id = x.Id, + FirmwareVersion = x.FirmwareVersion, + Gateway = x.Gateway, + Owner = dbItem.Owner, + Name = dbItem.Name, + ConnectedAt = x.ConnectedAt, + UserAgent = x.UserAgent, + BootedAt = x.BootedAt, + LatencyMs = x.LatencyMs, + Rssi = x.Rssi, + }; + }) + .ToArray() ); } diff --git a/API/Controller/Sessions/ListSessions.cs b/API/Controller/Sessions/ListSessions.cs index 27468f5e..bafc895a 100644 --- a/API/Controller/Sessions/ListSessions.cs +++ b/API/Controller/Sessions/ListSessions.cs @@ -8,11 +8,11 @@ namespace OpenShock.API.Controller.Sessions; public sealed partial class SessionsController { [HttpGet] - [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] - public async Task> ListSessions() + [ProducesResponseType(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] + public async Task ListSessions() { var sessions = await _sessionService.ListSessionsByUserId(CurrentUser.Id); - return sessions.Select(LoginSessionResponse.MapFrom); + return sessions.Select(LoginSessionResponse.MapFrom).ToArray(); } } \ No newline at end of file diff --git a/API/Controller/Shares/V2GetShares.cs b/API/Controller/Shares/V2GetShares.cs index 93c061cd..6b26a408 100644 --- a/API/Controller/Shares/V2GetShares.cs +++ b/API/Controller/Shares/V2GetShares.cs @@ -15,7 +15,7 @@ namespace OpenShock.API.Controller.Shares; public sealed partial class SharesController { [HttpGet] - [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] [ApiVersion("2")] public async Task GetSharesByUsers() { diff --git a/API/Controller/Shares/V2Requests.cs b/API/Controller/Shares/V2Requests.cs index bf21e0e4..ad205a46 100644 --- a/API/Controller/Shares/V2Requests.cs +++ b/API/Controller/Shares/V2Requests.cs @@ -121,7 +121,7 @@ public async Task GetRequest(Guid id) Vibrate = y.PermVibrate, Live = y.PermLive } - }) + }).ToArray() }).FirstOrDefaultAsync(); if (outstandingShare == null) return Problem(ShareError.ShareRequestNotFound); @@ -199,5 +199,5 @@ public sealed class ShareRequestCounts public sealed class ShareRequestBaseDetails : ShareRequestBase { - public required IEnumerable Shockers { get; set; } + public required ShockerPermLimitPairWithId[] Shockers { get; set; } } \ No newline at end of file diff --git a/API/Controller/Shockers/ControlLogController.cs b/API/Controller/Shockers/ControlLogController.cs index 86d3e4f7..67c56ead 100644 --- a/API/Controller/Shockers/ControlLogController.cs +++ b/API/Controller/Shockers/ControlLogController.cs @@ -24,7 +24,7 @@ public sealed partial class ShockerController /// The logs /// Shocker does not exist [HttpGet("{shockerId}/logs")] - [ProducesResponseType>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] + [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] [ProducesResponseType(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // ShockerNotFound [MapToApiVersion("1")] public async Task GetShockerLogs([FromRoute] Guid shockerId, [FromQuery] uint offset = 0, diff --git a/API/Controller/Shockers/ControlShockerController.cs b/API/Controller/Shockers/ControlShockerController.cs index a67b991f..3e02cd50 100644 --- a/API/Controller/Shockers/ControlShockerController.cs +++ b/API/Controller/Shockers/ControlShockerController.cs @@ -65,7 +65,7 @@ public async Task SendControl( [ProducesResponseType(StatusCodes.Status412PreconditionFailed, MediaTypeNames.Application.ProblemJson)] // Shocker is paused [ProducesResponseType(StatusCodes.Status403Forbidden, MediaTypeNames.Application.ProblemJson)] // You don't have permission to control this shocker public Task SendControl_DEPRECATED( - [FromBody] IEnumerable body, + [FromBody] Common.Models.WebSocket.User.Control[] body, [FromServices] IHubContext userHub, [FromServices] IRedisPubService redisPubService) { diff --git a/API/Controller/Shockers/OwnShockerController.cs b/API/Controller/Shockers/OwnShockerController.cs index 62f17cce..57dd4d9e 100644 --- a/API/Controller/Shockers/OwnShockerController.cs +++ b/API/Controller/Shockers/OwnShockerController.cs @@ -15,7 +15,7 @@ public sealed partial class ShockerController /// /// The shockers were successfully retrieved. [HttpGet("own")] - [ProducesResponseType>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] + [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] [MapToApiVersion("1")] public async Task ListShockers() { @@ -25,15 +25,18 @@ public async Task ListShockers() Id = x.Id, Name = x.Name, CreatedOn = x.CreatedOn, - Shockers = x.Shockers.OrderBy(y => y.CreatedOn).Select(y => new ShockerResponse - { - Id = y.Id, - Name = y.Name, - RfId = y.RfId, - CreatedOn = y.CreatedOn, - Model = y.Model, - IsPaused = y.Paused - }) + Shockers = x.Shockers + .OrderBy(y => y.CreatedOn) + .Select(y => new ShockerResponse + { + Id = y.Id, + Name = y.Name, + RfId = y.RfId, + CreatedOn = y.CreatedOn, + Model = y.Model, + IsPaused = y.Paused + }) + .ToArray() }).ToArrayAsync(); return RespondSuccessLegacy(shockers); diff --git a/API/Controller/Shockers/ShareShockerController.cs b/API/Controller/Shockers/ShareShockerController.cs index fb5e81d0..838f0789 100644 --- a/API/Controller/Shockers/ShareShockerController.cs +++ b/API/Controller/Shockers/ShareShockerController.cs @@ -25,7 +25,7 @@ public sealed partial class ShockerController /// OK /// The shocker does not exist or you do not have access to it. [HttpGet("{shockerId}/shares")] - [ProducesResponseType>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] + [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] [ProducesResponseType(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // ShockerNotFound [MapToApiVersion("1")] public async Task GetShockerShares([FromRoute] Guid shockerId) @@ -68,7 +68,7 @@ public async Task GetShockerShares([FromRoute] Guid shockerId) /// /// OK [HttpGet("{shockerId}/shareCodes")] - [ProducesResponseType>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] + [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] [ProducesResponseType(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // ShockerNotFound [MapToApiVersion("1")] public async Task ShockerShareCodeList([FromRoute] Guid shockerId) diff --git a/API/Controller/Shockers/SharedShockersController.cs b/API/Controller/Shockers/SharedShockersController.cs index bd0ec8f0..670dc369 100644 --- a/API/Controller/Shockers/SharedShockersController.cs +++ b/API/Controller/Shockers/SharedShockersController.cs @@ -1,14 +1,23 @@ -using System.Net.Mime; -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using OpenShock.API.Models.Response; using OpenShock.Common.Models; -using OpenShock.Common.Problems; using OpenShock.Common.Utils; +using System.Net.Mime; namespace OpenShock.API.Controller.Shockers; +file record OwnerGroupKey(Guid OwnerId, string OwnerName, string OwnerEmail) +{ + public override int GetHashCode() => OwnerId.GetHashCode(); +} + +file record DeviceGroupKey(Guid DeviceId, string DeviceName) +{ + public override int GetHashCode() => DeviceId.GetHashCode(); +} + public sealed partial class ShockerController { /// @@ -16,12 +25,15 @@ public sealed partial class ShockerController /// /// The shockers were successfully retrieved. [HttpGet("shared")] - [ProducesResponseType>>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] + [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] [MapToApiVersion("1")] public async Task ListSharedShockers() { - var sharedShockersRaw = await _db.ShockerShares.Where(x => x.SharedWith == CurrentUser.Id).Select(x => - new + var sharedShockersData = await _db.ShockerShares + .AsNoTracking() + .Include(x => x.Shocker.DeviceNavigation.OwnerNavigation) + .Where(x => x.SharedWith == CurrentUser.Id) + .Select(x => new { OwnerId = x.Shocker.DeviceNavigation.OwnerNavigation.Id, OwnerName = x.Shocker.DeviceNavigation.OwnerNavigation.Name, @@ -46,33 +58,30 @@ public async Task ListSharedShockers() Intensity = x.LimitIntensity } } - }).ToArrayAsync(); - - var shared = new Dictionary(); - foreach (var shocker in sharedShockersRaw) - { - // No I dont want unnecessary alloc - // ReSharper disable once CanSimplifyDictionaryLookupWithTryAdd - if (!shared.ContainsKey(shocker.OwnerId)) - shared[shocker.OwnerId] = new OwnerShockerResponse - { - Id = shocker.OwnerId, - Name = shocker.OwnerName, - Image = GravatarUtils.GetUserImageUrl(shocker.OwnerEmail) - }; - - var sharedUser = shared[shocker.OwnerId]; + }) + .ToArrayAsync(); - if (sharedUser.Devices.All(x => x.Id != shocker.DeviceId)) - sharedUser.Devices.Add(new OwnerShockerResponse.SharedDevice - { - Id = shocker.DeviceId, - Name = shocker.DeviceName - }); - - sharedUser.Devices.Single(x => x.Id == shocker.DeviceId).Shockers.Add(shocker.Shocker); - } + var sharesResponse = sharedShockersData + .GroupBy(x => new OwnerGroupKey(x.OwnerId, x.OwnerName, x.OwnerEmail)) + .Select(ownerGroup => new OwnerShockerResponse + { + Id = ownerGroup.Key.OwnerId, + Name = ownerGroup.Key.OwnerName, + Image = GravatarUtils.GetUserImageUrl(ownerGroup.Key.OwnerEmail), + Devices = ownerGroup + .GroupBy(x => new DeviceGroupKey(x.DeviceId, x.DeviceName)) + .Select(deviceGroup => new OwnerShockerResponse.SharedDevice + { + Id = deviceGroup.Key.DeviceId, + Name = deviceGroup.Key.DeviceName, + Shockers = deviceGroup + .Select(x => x.Shocker) + .ToArray(), + }) + .ToArray() + }) + .ToArray(); - return RespondSuccessLegacy(shared.Values); + return RespondSuccessLegacy(sharesResponse); } } \ No newline at end of file diff --git a/API/Controller/Tokens/TokenController.cs b/API/Controller/Tokens/TokenController.cs index 7e6d9b41..3d457b29 100644 --- a/API/Controller/Tokens/TokenController.cs +++ b/API/Controller/Tokens/TokenController.cs @@ -25,8 +25,8 @@ public sealed partial class TokensController /// /// All tokens for the current user [HttpGet] - [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] - public async Task> ListTokens() + [ProducesResponseType(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] + public async Task ListTokens() { var apiTokens = await _db.ApiTokens .Where(x => x.UserId == CurrentUser.Id && (x.ValidUntil == null || x.ValidUntil > DateTime.UtcNow)) @@ -39,7 +39,7 @@ public async Task> ListTokens() Permissions = x.Permissions, Name = x.Name, Id = x.Id - }).ToListAsync(); + }).ToArrayAsync(); return apiTokens; } diff --git a/API/Models/Requests/CreateShareRequest.cs b/API/Models/Requests/CreateShareRequest.cs index 41136551..93ff451e 100644 --- a/API/Models/Requests/CreateShareRequest.cs +++ b/API/Models/Requests/CreateShareRequest.cs @@ -6,7 +6,7 @@ namespace OpenShock.API.Models.Requests; public sealed class CreateShareRequest { [MaxLength(HardLimits.CreateShareRequestMaxShockers)] - public required IEnumerable Shockers { get; set; } + public required ShockerPermLimitPairWithId[] Shockers { get; set; } public Guid? User { get; set; } = null; } diff --git a/API/Models/Response/DeviceSelfResponse.cs b/API/Models/Response/DeviceSelfResponse.cs index 53ce58d6..dd448dd7 100644 --- a/API/Models/Response/DeviceSelfResponse.cs +++ b/API/Models/Response/DeviceSelfResponse.cs @@ -4,5 +4,5 @@ public sealed class DeviceSelfResponse { public required Guid Id { get; set; } public required string Name { get; set; } - public required IEnumerable Shockers { get; set; } + public required MinimalShocker[] Shockers { get; set; } } \ No newline at end of file diff --git a/API/Models/Response/OwnerShockerResponse.cs b/API/Models/Response/OwnerShockerResponse.cs index 3318d27e..3270fb6d 100644 --- a/API/Models/Response/OwnerShockerResponse.cs +++ b/API/Models/Response/OwnerShockerResponse.cs @@ -4,14 +4,14 @@ namespace OpenShock.API.Models.Response; public sealed class OwnerShockerResponse : GenericIni { - public IList Devices { get; set; } = new List(); + public required SharedDevice[] Devices { get; set; } public sealed class SharedDevice { public required Guid Id { get; set; } public required string Name { get; set; } // ReSharper disable once CollectionNeverQueried.Global - public IList Shockers { get; set; } = new List(); + public required SharedShocker[] Shockers { get; set; } public sealed class SharedShocker { diff --git a/API/Models/Response/ResponseDeviceWithShockers.cs b/API/Models/Response/ResponseDeviceWithShockers.cs index 0c343f97..435128f1 100644 --- a/API/Models/Response/ResponseDeviceWithShockers.cs +++ b/API/Models/Response/ResponseDeviceWithShockers.cs @@ -2,5 +2,5 @@ public sealed class ResponseDeviceWithShockers : ResponseDevice { - public required IEnumerable Shockers { get; set; } + public required ShockerResponse[] Shockers { get; set; } } \ No newline at end of file diff --git a/API/Models/Response/UserSharesResponse.cs b/API/Models/Response/UserSharesResponse.cs index 087792ec..83597ea0 100644 --- a/API/Models/Response/UserSharesResponse.cs +++ b/API/Models/Response/UserSharesResponse.cs @@ -5,7 +5,7 @@ namespace OpenShock.API.Models.Response; public sealed class UserSharesResponse { - public required IEnumerable Shockers { get; init; } + public required UserShareInfo[] Shockers { get; init; } } public sealed class UserShareInfo : GenericIn diff --git a/API/Services/Email/Mailjet/Mail/MailBase.cs b/API/Services/Email/Mailjet/Mail/MailBase.cs index 67d38479..ef927eab 100644 --- a/API/Services/Email/Mailjet/Mail/MailBase.cs +++ b/API/Services/Email/Mailjet/Mail/MailBase.cs @@ -7,7 +7,7 @@ namespace OpenShock.API.Services.Email.Mailjet.Mail; public abstract class MailBase { public required Contact From { get; set; } - public required IEnumerable To { get; set; } + public required Contact[] To { get; set; } public required string Subject { get; set; } public Dictionary? Variables { get; set; } } \ No newline at end of file diff --git a/API/Services/Email/Mailjet/Mail/MailsWrap.cs b/API/Services/Email/Mailjet/Mail/MailsWrap.cs index ff2fa17b..23f646d0 100644 --- a/API/Services/Email/Mailjet/Mail/MailsWrap.cs +++ b/API/Services/Email/Mailjet/Mail/MailsWrap.cs @@ -2,5 +2,5 @@ public sealed class MailsWrap { - public required IEnumerable Messages { get; set; } + public required MailBase[] Messages { get; set; } } \ No newline at end of file diff --git a/API/Services/Email/Mailjet/MailjetEmailService.cs b/API/Services/Email/Mailjet/MailjetEmailService.cs index daccd4ac..67e227e6 100644 --- a/API/Services/Email/Mailjet/MailjetEmailService.cs +++ b/API/Services/Email/Mailjet/MailjetEmailService.cs @@ -73,14 +73,14 @@ await SendMail(new TemplateMail #endregion - private Task SendMail(MailBase templateMail, CancellationToken cancellationToken = default) => SendMails(new[] { templateMail }, cancellationToken); + private Task SendMail(MailBase templateMail, CancellationToken cancellationToken = default) => SendMails([templateMail], cancellationToken); private static readonly JsonSerializerOptions Options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - private async Task SendMails(IEnumerable mails, CancellationToken cancellationToken = default) + private async Task SendMails(MailBase[] mails, CancellationToken cancellationToken = default) { if (_logger.IsEnabled(LogLevel.Debug)) _logger.LogDebug("Sending mails {@Mails}", mails); diff --git a/Common/Authentication/OpenShockAuthorizationMiddlewareResultHandler.cs b/Common/Authentication/OpenShockAuthorizationMiddlewareResultHandler.cs index bc4c84ae..db5fe733 100644 --- a/Common/Authentication/OpenShockAuthorizationMiddlewareResultHandler.cs +++ b/Common/Authentication/OpenShockAuthorizationMiddlewareResultHandler.cs @@ -25,7 +25,7 @@ public Task HandleAsync(RequestDelegate next, HttpContext context, Authorization { if (authorizeResult.Forbidden) { - var failedRequirements = authorizeResult.AuthorizationFailure?.FailedRequirements.Select(x => x.ToString() ?? "error") ?? Array.Empty(); + var failedRequirements = authorizeResult.AuthorizationFailure?.FailedRequirements.Select(x => x.ToString() ?? "error").ToArray() ?? []; var problem = AuthorizationError.PolicyNotMet(failedRequirements); context.Response.StatusCode = problem.Status!.Value; problem.AddContext(context); diff --git a/Common/Errors/AuthorizationError.cs b/Common/Errors/AuthorizationError.cs index a0d4dc7b..c19e6c34 100644 --- a/Common/Errors/AuthorizationError.cs +++ b/Common/Errors/AuthorizationError.cs @@ -18,9 +18,9 @@ public static class AuthorizationError "This endpoint is only available to use with api tokens", HttpStatusCode.Forbidden); public static TokenPermissionProblem TokenPermissionMissing(PermissionType requiredPermission, - IEnumerable grantedPermissions) => new("Authorization.Token.PermissionMissing", + List grantedPermissions) => new("Authorization.Token.PermissionMissing", $"You do not have the required permission to access this endpoint. Missing permission: {requiredPermission.ToString()}", requiredPermission, grantedPermissions, HttpStatusCode.Forbidden); - public static PolicyNotMetProblem PolicyNotMet(IEnumerable failedRequirements) => new PolicyNotMetProblem(failedRequirements); + public static PolicyNotMetProblem PolicyNotMet(string[] failedRequirements) => new PolicyNotMetProblem(failedRequirements); } \ No newline at end of file diff --git a/Common/Errors/ShareError.cs b/Common/Errors/ShareError.cs index 99e8279c..5ff07cef 100644 --- a/Common/Errors/ShareError.cs +++ b/Common/Errors/ShareError.cs @@ -8,7 +8,7 @@ public static class ShareError { public static OpenShockProblem ShareRequestNotFound => new("Share.Request.NotFound", "Share request not found", HttpStatusCode.NotFound); public static OpenShockProblem ShareRequestCreateCannotShareWithSelf => new("Share.Request.Create.CannotShareWithSelf", "You cannot share something with yourself", HttpStatusCode.BadRequest); - public static ShockersNotFoundProblem ShareCreateShockerNotFound(IEnumerable missingShockers) => new("Share.Request.Create.ShockerNotFound", "One or multiple of the provided shocker's were not found or do not belong to you", missingShockers, HttpStatusCode.NotFound); + public static ShockersNotFoundProblem ShareCreateShockerNotFound(Guid[] missingShockers) => new("Share.Request.Create.ShockerNotFound", "One or multiple of the provided shocker's were not found or do not belong to you", missingShockers, HttpStatusCode.NotFound); public static OpenShockProblem ShareGetNoShares => new("Share.Get.NoShares", "You have no shares with the specified user, or the user doesnt exist", HttpStatusCode.NotFound); } \ No newline at end of file diff --git a/Common/Models/ControlRequest.cs b/Common/Models/ControlRequest.cs index 49b1028e..1e88a902 100644 --- a/Common/Models/ControlRequest.cs +++ b/Common/Models/ControlRequest.cs @@ -2,6 +2,6 @@ public sealed class ControlRequest { - public required IEnumerable Shocks { get; set; } + public required WebSocket.User.Control[] Shocks { get; set; } public string? CustomName { get; set; } = null; } \ No newline at end of file diff --git a/Common/Models/Paginated.cs b/Common/Models/Paginated.cs index e447fd81..be8bab48 100644 --- a/Common/Models/Paginated.cs +++ b/Common/Models/Paginated.cs @@ -5,5 +5,5 @@ public sealed class Paginated public required int Offset { get; set; } public required int Limit { get; set; } public required long Total { get; set; } - public required IEnumerable Data { get; set; } + public required T[] Data { get; set; } } \ No newline at end of file diff --git a/Common/Models/PermissionType.cs b/Common/Models/PermissionType.cs index 6f804e90..040dd85c 100644 --- a/Common/Models/PermissionType.cs +++ b/Common/Models/PermissionType.cs @@ -23,16 +23,16 @@ public enum PermissionType public static class PermissionTypeExtensions { - public static bool IsAllowed(this PermissionType permissionType, IEnumerable permissions) => + public static bool IsAllowed(this PermissionType permissionType, IReadOnlyCollection permissions) => IsAllowedInternal(permissions, permissionType); - public static bool IsAllowed(this IEnumerable permissions, PermissionType permissionType) => + public static bool IsAllowed(this IReadOnlyCollection permissions, PermissionType permissionType) => IsAllowedInternal(permissions, permissionType); public static bool IsAllowedAllowOnNull(this IReadOnlyCollection? permissions, PermissionType permissionType) => permissions == null || IsAllowedInternal(permissions, permissionType); - private static bool IsAllowedInternal(IEnumerable permissions, PermissionType permissionType) + private static bool IsAllowedInternal(IReadOnlyCollection permissions, PermissionType permissionType) { // ReSharper disable once PossibleMultipleEnumeration return permissions.Contains(permissionType) || permissions.Any(x => @@ -58,24 +58,30 @@ public static class PermissionTypeBindings static PermissionTypeBindings() { - var bindings = Init().ToArray(); + var bindings = GetBindings(); PermissionTypeToName = bindings.ToDictionary(x => x.PermissionType, x => x); NameToPermissionType = bindings.ToDictionary(x => x.Name, x => x); } - private static IEnumerable Init() + private static PermissionTypeRecord[] GetBindings() { - var enumValues = Enum.GetValues(); - var fields = typeof(PermissionType).GetFields(); + var permissionTypes = Enum.GetValues(); + var permissionTypeFields = typeof(PermissionType).GetFields(); - foreach (var permissionType in enumValues) + var bindings = new PermissionTypeRecord[permissionTypes.Length]; + + for (var i = 0; i < permissionTypes.Length; i++) { - var field = fields.First(x => x.Name == permissionType.ToString()); + var permissionType = permissionTypes[i]; + + var field = permissionTypeFields.First(x => x.Name == permissionType.ToString()); var parents = field.GetCustomAttributes().Select(x => x.PermissionType); var name = field.GetCustomAttribute()!.PgName; - yield return new PermissionTypeRecord(permissionType, name, parents.ToArray()); + bindings[i] = new PermissionTypeRecord(permissionType, name, parents.ToArray()); } + + return bindings; } } diff --git a/Common/Problems/CustomProblems/PolicyNotMetProblem.cs b/Common/Problems/CustomProblems/PolicyNotMetProblem.cs index bc98e1ab..7f989c55 100644 --- a/Common/Problems/CustomProblems/PolicyNotMetProblem.cs +++ b/Common/Problems/CustomProblems/PolicyNotMetProblem.cs @@ -5,12 +5,12 @@ namespace OpenShock.Common.Problems.CustomProblems; public class PolicyNotMetProblem : OpenShockProblem { - public PolicyNotMetProblem(IEnumerable failedRequirements) : base( + public PolicyNotMetProblem(string[] failedRequirements) : base( "Authorization.Policy.NotMet", "One or multiple policies were not met", HttpStatusCode.Forbidden, string.Empty) { FailedRequirements = failedRequirements; } - public IEnumerable FailedRequirements { get; set; } + public string[] FailedRequirements { get; set; } } \ No newline at end of file diff --git a/Common/Problems/CustomProblems/ShockersNotFoundProblem.cs b/Common/Problems/CustomProblems/ShockersNotFoundProblem.cs index e3acbcb4..bddded97 100644 --- a/Common/Problems/CustomProblems/ShockersNotFoundProblem.cs +++ b/Common/Problems/CustomProblems/ShockersNotFoundProblem.cs @@ -5,10 +5,10 @@ namespace OpenShock.Common.Problems.CustomProblems; public sealed class ShockersNotFoundProblem( string type, string title, - IEnumerable missingShockers, + Guid[] missingShockers, HttpStatusCode status = HttpStatusCode.BadRequest, string? detail = null) : OpenShockProblem(type, title, status, detail) { - public IEnumerable MissingShockers { get; set; } = missingShockers; + public Guid[] MissingShockers { get; set; } = missingShockers; } \ No newline at end of file diff --git a/Common/Problems/CustomProblems/TokenPermissionProblem.cs b/Common/Problems/CustomProblems/TokenPermissionProblem.cs index d23b0598..bfdcc96d 100644 --- a/Common/Problems/CustomProblems/TokenPermissionProblem.cs +++ b/Common/Problems/CustomProblems/TokenPermissionProblem.cs @@ -7,11 +7,11 @@ public sealed class TokenPermissionProblem( string type, string title, PermissionType requiredPermission, - IEnumerable grantedPermissions, + List grantedPermissions, HttpStatusCode status = HttpStatusCode.BadRequest, string? detail = null) : OpenShockProblem(type, title, status, detail) { public PermissionType RequiredPermission { get; set; } = requiredPermission; - public IEnumerable GrantedPermissions { get; set; } = grantedPermissions; + public List GrantedPermissions { get; set; } = grantedPermissions; } \ No newline at end of file diff --git a/Common/Services/Session/ISessionService.cs b/Common/Services/Session/ISessionService.cs index 0fa5cbe4..9d0db7ee 100644 --- a/Common/Services/Session/ISessionService.cs +++ b/Common/Services/Session/ISessionService.cs @@ -6,7 +6,7 @@ public interface ISessionService { public Task CreateSessionAsync(string sessionId, Guid userId, string userAgent, string ipAddress); - public Task> ListSessionsByUserId(Guid userId); + public Task ListSessionsByUserId(Guid userId); public Task GetSessionById(string sessionId); diff --git a/Common/Services/Session/SessionService.cs b/Common/Services/Session/SessionService.cs index 5ca5cc78..d9b4861c 100644 --- a/Common/Services/Session/SessionService.cs +++ b/Common/Services/Session/SessionService.cs @@ -1,4 +1,5 @@ -using OpenShock.Common.Authentication.AuthenticationHandlers; +using Microsoft.EntityFrameworkCore; +using OpenShock.Common.Authentication.AuthenticationHandlers; using OpenShock.Common.Constants; using OpenShock.Common.Redis; using Redis.OM; @@ -41,9 +42,9 @@ await _loginSessions.InsertAsync(new LoginSession return publicId; } - public async Task> ListSessionsByUserId(Guid userId) + public async Task ListSessionsByUserId(Guid userId) { - var sessions = await _loginSessions.Where(x => x.UserId == userId).ToListAsync(); + var sessions = await _loginSessions.Where(x => x.UserId == userId).ToArrayAsync(); var needsSave = false; foreach (var session in sessions) diff --git a/Common/Utils/TrustedProxiesFetcher.cs b/Common/Utils/TrustedProxiesFetcher.cs index 9a854520..339fdc76 100644 --- a/Common/Utils/TrustedProxiesFetcher.cs +++ b/Common/Utils/TrustedProxiesFetcher.cs @@ -58,7 +58,7 @@ public static class TrustedProxiesFetcher private static readonly char[] NewLineSeperators = [' ', '\r', '\n', '\t']; - private static async Task> FetchCloudflareIPs(Uri uri, CancellationToken ct) + private static async Task> FetchCloudflareIPs(Uri uri, CancellationToken ct) { using var response = await Client.GetAsync(uri, ct); var stringResponse = await response.Content.ReadAsStringAsync(ct); @@ -66,11 +66,12 @@ private static async Task> FetchCloudflareIPs(Uri uri, Ca return ParseNetworks(stringResponse.AsSpan()); } - private static IEnumerable ParseNetworks(ReadOnlySpan response) + private static List ParseNetworks(ReadOnlySpan response) { var ranges = response.Split(NewLineSeperators); var networks = new List(); + foreach (var range in ranges) { networks.Add(IPNetwork.Parse(response[range])); @@ -79,7 +80,7 @@ private static IEnumerable ParseNetworks(ReadOnlySpan response) return networks; } - private static async Task> FetchCloudflareIPs() + private static async Task FetchCloudflareIPs() { try { diff --git a/Common/Websocket/WebsocketCollection.cs b/Common/Websocket/WebsocketCollection.cs index 9bd26c7e..c2842558 100644 --- a/Common/Websocket/WebsocketCollection.cs +++ b/Common/Websocket/WebsocketCollection.cs @@ -66,11 +66,9 @@ public async ValueTask SendMessageToAll(T msg) await websocketController.QueueMessage(msg); } - public IEnumerable> GetConnectedById(IEnumerable ids) + public IWebsocketController[] GetConnectedById(IEnumerable ids) { - var found = new List>(); - foreach (var id in ids) found.AddRange(GetConnections(id)); - return found; + return ids.SelectMany(GetConnections).ToArray(); } public uint Count => (uint)_websockets.Sum(x => x.Value.Count); From ba27c3f6cb9b50fadb25e9b4c736e0a75004e54d Mon Sep 17 00:00:00 2001 From: hhvrc Date: Thu, 13 Feb 2025 14:52:37 +0100 Subject: [PATCH 2/9] Remove the rest unnecessary --- Common/DeviceControl/ControlLogic.cs | 26 +++++++++++-------- Common/Hubs/IUserHub.cs | 2 +- Common/Hubs/ShareLinkHub.cs | 2 +- Common/Hubs/UserHub.cs | 4 +-- .../LifetimeManager/HubLifetime.cs | 2 +- .../LifetimeManager/HubLifetimeManager.cs | 2 +- 6 files changed, 21 insertions(+), 17 deletions(-) diff --git a/Common/DeviceControl/ControlLogic.cs b/Common/DeviceControl/ControlLogic.cs index 8befd7a9..2c6fc306 100644 --- a/Common/DeviceControl/ControlLogic.cs +++ b/Common/DeviceControl/ControlLogic.cs @@ -14,7 +14,7 @@ namespace OpenShock.Common.DeviceControl; public static class ControlLogic { - public static async Task> ControlByUser(IEnumerable shocks, OpenShockContext db, ControlLogSender sender, + public static async Task> ControlByUser(Control[] shocks, OpenShockContext db, ControlLogSender sender, IHubClients hubClients, IRedisPubService redisPubService) { var ownShockers = await db.Shockers.Where(x => x.DeviceNavigation.Owner == sender.Id).Select(x => @@ -55,7 +55,7 @@ public static async Task> ControlShareLink(IEnumerable shocks, OpenShockContext db, + public static async Task> ControlShareLink(Control[] shocks, OpenShockContext db, ControlLogSender sender, IHubClients hubClients, Guid shareLinkId, IRedisPubService redisPubService) { @@ -82,7 +82,7 @@ public static async Task> ControlInternal(IEnumerable shocks, OpenShockContext db, ControlLogSender sender, + private static async Task> ControlInternal(Control[] shocks, OpenShockContext db, ControlLogSender sender, IHubClients hubClients, IReadOnlyCollection allowedShockers, IRedisPubService redisPubService) { var finalMessages = new Dictionary>(); @@ -102,16 +102,10 @@ private static async Task deviceOnlineStates); + Task DeviceStatus(IList deviceOnlineStates); Task Log(ControlLogSender sender, IEnumerable logs); Task DeviceUpdate(Guid deviceId, DeviceUpdateType type); diff --git a/Common/Hubs/ShareLinkHub.cs b/Common/Hubs/ShareLinkHub.cs index bd26a705..10e0e0d2 100644 --- a/Common/Hubs/ShareLinkHub.cs +++ b/Common/Hubs/ShareLinkHub.cs @@ -112,7 +112,7 @@ public override async Task OnConnectedAsync() await Clients.Caller.Welcome(user != null ? AuthType.Authenticated : AuthType.Guest); } - public Task Control(IEnumerable shocks) + public Task Control(Models.WebSocket.User.Control[] shocks) { if (!_tokenPermissions.IsAllowedAllowOnNull(PermissionType.Shockers_Use)) return Task.CompletedTask; diff --git a/Common/Hubs/UserHub.cs b/Common/Hubs/UserHub.cs index c8383df2..c0b573d9 100644 --- a/Common/Hubs/UserHub.cs +++ b/Common/Hubs/UserHub.cs @@ -69,12 +69,12 @@ public override async Task OnConnectedAsync() await Clients.Caller.DeviceStatus(final); } - public Task Control(IEnumerable shocks) + public Task Control(Models.WebSocket.User.Control[] shocks) { return ControlV2(shocks, null); } - public async Task ControlV2(IEnumerable shocks, string? customName) + public async Task ControlV2(Models.WebSocket.User.Control[] shocks, string? customName) { if (!_tokenPermissions.IsAllowedAllowOnNull(PermissionType.Shockers_Use)) return; diff --git a/LiveControlGateway/LifetimeManager/HubLifetime.cs b/LiveControlGateway/LifetimeManager/HubLifetime.cs index b8ce9c28..cec8bf09 100644 --- a/LiveControlGateway/LifetimeManager/HubLifetime.cs +++ b/LiveControlGateway/LifetimeManager/HubLifetime.cs @@ -260,7 +260,7 @@ private static DateTimeOffset CalculateActiveUntil(byte tps) => /// /// /// - public ValueTask Control(IEnumerable shocks) + public ValueTask Control(IList shocks) { var shocksTransformed = new List(); foreach (var shock in shocks) diff --git a/LiveControlGateway/LifetimeManager/HubLifetimeManager.cs b/LiveControlGateway/LifetimeManager/HubLifetimeManager.cs index 361ac586..3f2b370d 100644 --- a/LiveControlGateway/LifetimeManager/HubLifetimeManager.cs +++ b/LiveControlGateway/LifetimeManager/HubLifetimeManager.cs @@ -214,7 +214,7 @@ public async Task> UpdateDevice(Guid device) /// /// public async Task> Control(Guid device, - IEnumerable shocks) + IList shocks) { if (!_lifetimes.TryGetValue(device, out var deviceLifetime)) return new DeviceNotFound(); await deviceLifetime.Control(shocks); From 5c1578efc0ee9e75fb421727a3579e1e8953cf99 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Thu, 13 Feb 2025 15:45:37 +0100 Subject: [PATCH 3/9] Implement back direct enumerable returns --- API/Controller/Sessions/ListSessions.cs | 6 +-- API/Controller/Shares/V2GetShares.cs | 19 +++++--- API/Controller/Shares/V2Requests.cs | 60 ++++++++++++------------ API/Controller/Tokens/TokenController.cs | 25 +++++----- 4 files changed, 57 insertions(+), 53 deletions(-) diff --git a/API/Controller/Sessions/ListSessions.cs b/API/Controller/Sessions/ListSessions.cs index bafc895a..27468f5e 100644 --- a/API/Controller/Sessions/ListSessions.cs +++ b/API/Controller/Sessions/ListSessions.cs @@ -8,11 +8,11 @@ namespace OpenShock.API.Controller.Sessions; public sealed partial class SessionsController { [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] - public async Task ListSessions() + [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] + public async Task> ListSessions() { var sessions = await _sessionService.ListSessionsByUserId(CurrentUser.Id); - return sessions.Select(LoginSessionResponse.MapFrom).ToArray(); + return sessions.Select(LoginSessionResponse.MapFrom); } } \ No newline at end of file diff --git a/API/Controller/Shares/V2GetShares.cs b/API/Controller/Shares/V2GetShares.cs index 6b26a408..7657a64a 100644 --- a/API/Controller/Shares/V2GetShares.cs +++ b/API/Controller/Shares/V2GetShares.cs @@ -15,18 +15,21 @@ namespace OpenShock.API.Controller.Shares; public sealed partial class SharesController { [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] + [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] [ApiVersion("2")] - public async Task GetSharesByUsers() + public IAsyncEnumerable GetSharesByUsers() { - var sharedToUsers = await _db.ShockerShares.Where(x => x.Shocker.DeviceNavigation.Owner == CurrentUser.Id) + return _db.ShockerShares + .Where(x => x.Shocker.DeviceNavigation.Owner == CurrentUser.Id) .Select(x => new GenericIni { Id = x.SharedWithNavigation.Id, Image = x.SharedWithNavigation.GetImageUrl(), Name = x.SharedWithNavigation.Name - }).OrderBy(x => x.Name).Distinct().ToArrayAsync(); - return sharedToUsers; + }) + .OrderBy(x => x.Name) + .Distinct() + .AsAsyncEnumerable(); } [HttpGet("{userId:guid}")] @@ -35,7 +38,8 @@ public async Task GetSharesByUsers() [ApiVersion("2")] public async Task GetSharesToUser(Guid userId) { - var sharedWithUser = await _db.ShockerShares.Where(x => x.Shocker.DeviceNavigation.Owner == CurrentUser.Id && x.SharedWith == userId) + var sharedWithUser = await _db.ShockerShares + .Where(x => x.Shocker.DeviceNavigation.Owner == CurrentUser.Id && x.SharedWith == userId) .Select(x => new UserShareInfo { Id = x.Shocker.Id, @@ -54,7 +58,8 @@ public async Task GetSharesToUser(Guid userId) Intensity = x.LimitIntensity }, Paused = x.Paused - }).ToArrayAsync(); + }) + .ToArrayAsync(); if(sharedWithUser.Length == 0) { diff --git a/API/Controller/Shares/V2Requests.cs b/API/Controller/Shares/V2Requests.cs index ad205a46..45177239 100644 --- a/API/Controller/Shares/V2Requests.cs +++ b/API/Controller/Shares/V2Requests.cs @@ -16,44 +16,45 @@ namespace OpenShock.API.Controller.Shares; public sealed partial class SharesController { [HttpGet("requests/outstanding")] - [ProducesResponseType(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] + [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] [ApiVersion("2")] - public async Task GetOutstandingRequestsList() + public IAsyncEnumerable GetOutstandingRequestsList() { - var outstandingShares = await _db.ShareRequests.Where(x => x.Owner == CurrentUser.Id) + return _db.ShareRequests + .Where(x => x.Owner == CurrentUser.Id) .Select(x => new ShareRequestBaseItem() - { - Id = x.Id, - CreatedOn = x.CreatedOn, - Owner = new GenericIni { - Id = x.OwnerNavigation.Id, - Name = x.OwnerNavigation.Name, - Image = x.OwnerNavigation.GetImageUrl() - }, - SharedWith = x.UserNavigation == null - ? null - : new GenericIni + Id = x.Id, + CreatedOn = x.CreatedOn, + Owner = new GenericIni { - Id = x.UserNavigation.Id, - Name = x.UserNavigation.Name, - Image = x.UserNavigation.GetImageUrl() + Id = x.OwnerNavigation.Id, + Name = x.OwnerNavigation.Name, + Image = x.OwnerNavigation.GetImageUrl() }, - Counts = new ShareRequestBaseItem.ShareRequestCounts - { - Shockers = x.ShareRequestsShockers.Count - } - }).ToArrayAsync(); - - return outstandingShares; + SharedWith = x.UserNavigation == null + ? null + : new GenericIni + { + Id = x.UserNavigation.Id, + Name = x.UserNavigation.Name, + Image = x.UserNavigation.GetImageUrl() + }, + Counts = new ShareRequestBaseItem.ShareRequestCounts + { + Shockers = x.ShareRequestsShockers.Count + } + }) + .AsAsyncEnumerable(); } [HttpGet("requests/incoming")] - [ProducesResponseType(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] + [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] [ApiVersion("2")] - public async Task GetIncomingRequestsList() + public IAsyncEnumerable GetIncomingRequestsList() { - var outstandingShares = await _db.ShareRequests.Where(x => x.User == CurrentUser.Id) + return _db.ShareRequests + .Where(x => x.User == CurrentUser.Id) .Select(x => new ShareRequestBaseItem { Id = x.Id, @@ -76,9 +77,8 @@ public async Task GetIncomingRequestsList() { Shockers = x.ShareRequestsShockers.Count } - }).ToArrayAsync(); - - return outstandingShares; + }) + .AsAsyncEnumerable(); } [HttpGet("requests/{id:guid}")] diff --git a/API/Controller/Tokens/TokenController.cs b/API/Controller/Tokens/TokenController.cs index 3d457b29..184efeac 100644 --- a/API/Controller/Tokens/TokenController.cs +++ b/API/Controller/Tokens/TokenController.cs @@ -25,23 +25,22 @@ public sealed partial class TokensController /// /// All tokens for the current user [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] - public async Task ListTokens() + [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] + public IAsyncEnumerable ListTokens() { - var apiTokens = await _db.ApiTokens + return _db.ApiTokens .Where(x => x.UserId == CurrentUser.Id && (x.ValidUntil == null || x.ValidUntil > DateTime.UtcNow)) .OrderBy(x => x.CreatedOn) .Select(x => new TokenResponse - { - CreatedOn = x.CreatedOn, - ValidUntil = x.ValidUntil, - LastUsed = x.LastUsed, - Permissions = x.Permissions, - Name = x.Name, - Id = x.Id - }).ToArrayAsync(); - - return apiTokens; + { + CreatedOn = x.CreatedOn, + ValidUntil = x.ValidUntil, + LastUsed = x.LastUsed, + Permissions = x.Permissions, + Name = x.Name, + Id = x.Id + }) + .AsAsyncEnumerable(); } /// From 0aa76d03aecebef23a3cf95083420c326240b2f8 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Thu, 13 Feb 2025 16:03:00 +0100 Subject: [PATCH 4/9] Change controller return types to iasyncenumerable where applicable --- API/Controller/Admin/GetOnlineDevices.cs | 3 +- API/Controller/Devices/DevicesController.cs | 8 +++-- API/Controller/Devices/ShockersController.cs | 23 +++++++------- API/Controller/Shares/Links/ListShareLinks.cs | 8 +++-- .../Shockers/ControlLogController.cs | 13 +++++--- .../Shockers/OwnShockerController.cs | 10 ++++--- .../Shockers/ShareShockerController.cs | 30 +++++++++++-------- .../Shockers/SharedShockersController.cs | 5 ++-- 8 files changed, 58 insertions(+), 42 deletions(-) diff --git a/API/Controller/Admin/GetOnlineDevices.cs b/API/Controller/Admin/GetOnlineDevices.cs index d231d4b9..403a851c 100644 --- a/API/Controller/Admin/GetOnlineDevices.cs +++ b/API/Controller/Admin/GetOnlineDevices.cs @@ -20,7 +20,7 @@ public sealed partial class AdminController /// All online devices /// Unauthorized [HttpGet("monitoring/onlineDevices")] - [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] + [ProducesResponseType>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] public async Task GetOnlineDevices() { var devicesOnline = _redis.RedisCollection(false); @@ -60,7 +60,6 @@ public async Task GetOnlineDevices() Rssi = x.Rssi, }; }) - .ToArray() ); } diff --git a/API/Controller/Devices/DevicesController.cs b/API/Controller/Devices/DevicesController.cs index e30cf76a..7c1851a5 100644 --- a/API/Controller/Devices/DevicesController.cs +++ b/API/Controller/Devices/DevicesController.cs @@ -23,17 +23,19 @@ public sealed partial class DevicesController /// /// All devices for the current user [HttpGet] - [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] + [ProducesResponseType>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] [MapToApiVersion("1")] public async Task ListDevices() { - var devices = await _db.Devices.Where(x => x.Owner == CurrentUser.Id) + var devices = _db.Devices + .Where(x => x.Owner == CurrentUser.Id) .Select(x => new Models.Response.ResponseDevice { Id = x.Id, Name = x.Name, CreatedOn = x.CreatedOn - }).ToArrayAsync(); + }) + .AsAsyncEnumerable(); return RespondSuccessLegacy(devices); } diff --git a/API/Controller/Devices/ShockersController.cs b/API/Controller/Devices/ShockersController.cs index 0f962f82..4283b9bf 100644 --- a/API/Controller/Devices/ShockersController.cs +++ b/API/Controller/Devices/ShockersController.cs @@ -19,7 +19,7 @@ public sealed partial class DevicesController /// All shockers for the device /// Device does not exists or you do not have access to it. [HttpGet("{deviceId}/shockers")] - [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] + [ProducesResponseType>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] [ProducesResponseType(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // DeviceNotFound [MapToApiVersion("1")] public async Task GetShockers([FromRoute] Guid deviceId) @@ -27,15 +27,18 @@ public async Task GetShockers([FromRoute] Guid deviceId) var deviceExists = await _db.Devices.AnyAsync(x => x.Owner == CurrentUser.Id && x.Id == deviceId); if (!deviceExists) return Problem(DeviceError.DeviceNotFound); - var shockers = await _db.Shockers.Where(x => x.Device == deviceId).Select(x => new ShockerResponse - { - Id = x.Id, - Name = x.Name, - RfId = x.RfId, - CreatedOn = x.CreatedOn, - Model = x.Model, - IsPaused = x.Paused - }).ToArrayAsync(); + var shockers = _db.Shockers + .Where(x => x.Device == deviceId) + .Select(x => new ShockerResponse + { + Id = x.Id, + Name = x.Name, + RfId = x.RfId, + CreatedOn = x.CreatedOn, + Model = x.Model, + IsPaused = x.Paused + }) + .AsAsyncEnumerable(); return RespondSuccessLegacy(shockers); } diff --git a/API/Controller/Shares/Links/ListShareLinks.cs b/API/Controller/Shares/Links/ListShareLinks.cs index 63d57538..b406ee65 100644 --- a/API/Controller/Shares/Links/ListShareLinks.cs +++ b/API/Controller/Shares/Links/ListShareLinks.cs @@ -14,11 +14,13 @@ public sealed partial class ShareLinksController /// /// All share links for the current user [HttpGet] - [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] + [ProducesResponseType>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] public async Task List() { - var ownShareLinks = await _db.ShockerSharesLinks.Where(x => x.OwnerId == CurrentUser.Id) - .Select(x => ShareLinkResponse.GetFromEf(x)).ToArrayAsync(); + var ownShareLinks = _db.ShockerSharesLinks + .Where(x => x.OwnerId == CurrentUser.Id) + .Select(x => ShareLinkResponse.GetFromEf(x)) + .AsAsyncEnumerable(); return RespondSuccessLegacy(ownShareLinks); } diff --git a/API/Controller/Shockers/ControlLogController.cs b/API/Controller/Shockers/ControlLogController.cs index 67c56ead..28990325 100644 --- a/API/Controller/Shockers/ControlLogController.cs +++ b/API/Controller/Shockers/ControlLogController.cs @@ -24,7 +24,7 @@ public sealed partial class ShockerController /// The logs /// Shocker does not exist [HttpGet("{shockerId}/logs")] - [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] + [ProducesResponseType>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] [ProducesResponseType(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // ShockerNotFound [MapToApiVersion("1")] public async Task GetShockerLogs([FromRoute] Guid shockerId, [FromQuery] uint offset = 0, @@ -33,8 +33,12 @@ [FromQuery] [Range(1, 500)] uint limit = 100) var exists = await _db.Shockers.AnyAsync(x => x.DeviceNavigation.Owner == CurrentUser.Id && x.Id == shockerId); if (!exists) return Problem(ShockerError.ShockerNotFound); - var logs = await _db.ShockerControlLogs.Where(x => x.ShockerId == shockerId) - .OrderByDescending(x => x.CreatedOn).Skip((int)offset).Take((int)limit).Select(x => new LogEntry + var logs = _db.ShockerControlLogs + .Where(x => x.ShockerId == shockerId) + .OrderByDescending(x => x.CreatedOn) + .Skip((int)offset) + .Take((int)limit) + .Select(x => new LogEntry { Id = x.Id, Duration = x.Duration, @@ -56,7 +60,8 @@ [FromQuery] [Range(1, 500)] uint limit = 100) Image = x.ControlledByNavigation.GetImageUrl(), CustomName = x.CustomName } - }).ToArrayAsync(); + }) + .AsAsyncEnumerable(); return RespondSuccessLegacy(logs); } diff --git a/API/Controller/Shockers/OwnShockerController.cs b/API/Controller/Shockers/OwnShockerController.cs index 57dd4d9e..b295a3cd 100644 --- a/API/Controller/Shockers/OwnShockerController.cs +++ b/API/Controller/Shockers/OwnShockerController.cs @@ -15,12 +15,13 @@ public sealed partial class ShockerController /// /// The shockers were successfully retrieved. [HttpGet("own")] - [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] + [ProducesResponseType>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] [MapToApiVersion("1")] public async Task ListShockers() { - var shockers = await _db.Devices.Where(x => x.Owner == CurrentUser.Id).OrderBy(x => x.CreatedOn).Select( - x => new ResponseDeviceWithShockers + var shockers = _db.Devices + .Where(x => x.Owner == CurrentUser.Id) + .OrderBy(x => x.CreatedOn).Select(x => new ResponseDeviceWithShockers { Id = x.Id, Name = x.Name, @@ -37,7 +38,8 @@ public async Task ListShockers() IsPaused = y.Paused }) .ToArray() - }).ToArrayAsync(); + }) + .AsAsyncEnumerable(); return RespondSuccessLegacy(shockers); } diff --git a/API/Controller/Shockers/ShareShockerController.cs b/API/Controller/Shockers/ShareShockerController.cs index 838f0789..4e8f7035 100644 --- a/API/Controller/Shockers/ShareShockerController.cs +++ b/API/Controller/Shockers/ShareShockerController.cs @@ -25,15 +25,17 @@ public sealed partial class ShockerController /// OK /// The shocker does not exist or you do not have access to it. [HttpGet("{shockerId}/shares")] - [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] + [ProducesResponseType>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] [ProducesResponseType(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // ShockerNotFound [MapToApiVersion("1")] public async Task GetShockerShares([FromRoute] Guid shockerId) { var owns = await _db.Shockers.AnyAsync(x => x.DeviceNavigation.Owner == CurrentUser.Id && x.Id == shockerId); if (!owns) return Problem(ShockerError.ShockerNotFound); - var shares = await _db.ShockerShares - .Where(x => x.ShockerId == shockerId && x.Shocker.DeviceNavigation.Owner == CurrentUser.Id).Select(x => + + var shares = _db.ShockerShares + .Where(x => x.ShockerId == shockerId && x.Shocker.DeviceNavigation.Owner == CurrentUser.Id) + .Select(x => new ShareInfo { Paused = x.Paused, @@ -57,7 +59,8 @@ public async Task GetShockerShares([FromRoute] Guid shockerId) Intensity = x.LimitIntensity } } - ).ToArrayAsync(); + ) + .AsAsyncEnumerable(); return RespondSuccessLegacy(shares); } @@ -68,21 +71,22 @@ public async Task GetShockerShares([FromRoute] Guid shockerId) /// /// OK [HttpGet("{shockerId}/shareCodes")] - [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] + [ProducesResponseType>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] [ProducesResponseType(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // ShockerNotFound [MapToApiVersion("1")] public async Task ShockerShareCodeList([FromRoute] Guid shockerId) { var owns = await _db.Shockers.AnyAsync(x => x.DeviceNavigation.Owner == CurrentUser.Id && x.Id == shockerId); if (!owns) return Problem(ShockerError.ShockerNotFound); - var shares = await _db.ShockerShareCodes - .Where(x => x.ShockerId == shockerId && x.Shocker.DeviceNavigation.Owner == CurrentUser.Id).Select(x => - new ShareCodeInfo - { - CreatedOn = x.CreatedOn, - Id = x.Id - } - ).ToArrayAsync(); + + var shares = _db.ShockerShareCodes + .Where(x => x.ShockerId == shockerId && x.Shocker.DeviceNavigation.Owner == CurrentUser.Id) + .Select(x => new ShareCodeInfo + { + CreatedOn = x.CreatedOn, + Id = x.Id + }) + .AsAsyncEnumerable(); return RespondSuccessLegacy(shares); } diff --git a/API/Controller/Shockers/SharedShockersController.cs b/API/Controller/Shockers/SharedShockersController.cs index 670dc369..7c71e8da 100644 --- a/API/Controller/Shockers/SharedShockersController.cs +++ b/API/Controller/Shockers/SharedShockersController.cs @@ -25,7 +25,7 @@ public sealed partial class ShockerController /// /// The shockers were successfully retrieved. [HttpGet("shared")] - [ProducesResponseType>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] + [ProducesResponseType>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] [MapToApiVersion("1")] public async Task ListSharedShockers() { @@ -79,8 +79,7 @@ public async Task ListSharedShockers() .ToArray(), }) .ToArray() - }) - .ToArray(); + }); return RespondSuccessLegacy(sharesResponse); } From 0871356d95ec28ae306c2d0bc253b8c6f5d00f30 Mon Sep 17 00:00:00 2001 From: hhvrc Date: Thu, 13 Feb 2025 16:09:29 +0100 Subject: [PATCH 5/9] Remove async on endpoint --- API/Controller/Shares/Links/ListShareLinks.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/API/Controller/Shares/Links/ListShareLinks.cs b/API/Controller/Shares/Links/ListShareLinks.cs index b406ee65..1c21fe92 100644 --- a/API/Controller/Shares/Links/ListShareLinks.cs +++ b/API/Controller/Shares/Links/ListShareLinks.cs @@ -15,7 +15,7 @@ public sealed partial class ShareLinksController /// All share links for the current user [HttpGet] [ProducesResponseType>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] - public async Task List() + public IActionResult List() { var ownShareLinks = _db.ShockerSharesLinks .Where(x => x.OwnerId == CurrentUser.Id) From f00578893684b00b360aadba2fd7dd753d3a0e31 Mon Sep 17 00:00:00 2001 From: LucHeart Date: Sun, 9 Mar 2025 17:12:10 +0100 Subject: [PATCH 6/9] Revert and adjust some changes to array --- .../OpenShockAuthorizationMiddlewareResultHandler.cs | 2 +- Common/Errors/AuthorizationError.cs | 4 ++-- Common/Problems/CustomProblems/PolicyNotMetProblem.cs | 5 ++--- .../Problems/CustomProblems/TokenPermissionProblem.cs | 4 ++-- Common/Utils/TrustedProxiesFetcher.cs | 10 +++++----- 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/Common/Authentication/OpenShockAuthorizationMiddlewareResultHandler.cs b/Common/Authentication/OpenShockAuthorizationMiddlewareResultHandler.cs index db5fe733..a403a5a9 100644 --- a/Common/Authentication/OpenShockAuthorizationMiddlewareResultHandler.cs +++ b/Common/Authentication/OpenShockAuthorizationMiddlewareResultHandler.cs @@ -25,7 +25,7 @@ public Task HandleAsync(RequestDelegate next, HttpContext context, Authorization { if (authorizeResult.Forbidden) { - var failedRequirements = authorizeResult.AuthorizationFailure?.FailedRequirements.Select(x => x.ToString() ?? "error").ToArray() ?? []; + var failedRequirements = authorizeResult.AuthorizationFailure?.FailedRequirements.Select(x => x.ToString() ?? "error") ?? []; var problem = AuthorizationError.PolicyNotMet(failedRequirements); context.Response.StatusCode = problem.Status!.Value; problem.AddContext(context); diff --git a/Common/Errors/AuthorizationError.cs b/Common/Errors/AuthorizationError.cs index c19e6c34..a0d4dc7b 100644 --- a/Common/Errors/AuthorizationError.cs +++ b/Common/Errors/AuthorizationError.cs @@ -18,9 +18,9 @@ public static class AuthorizationError "This endpoint is only available to use with api tokens", HttpStatusCode.Forbidden); public static TokenPermissionProblem TokenPermissionMissing(PermissionType requiredPermission, - List grantedPermissions) => new("Authorization.Token.PermissionMissing", + IEnumerable grantedPermissions) => new("Authorization.Token.PermissionMissing", $"You do not have the required permission to access this endpoint. Missing permission: {requiredPermission.ToString()}", requiredPermission, grantedPermissions, HttpStatusCode.Forbidden); - public static PolicyNotMetProblem PolicyNotMet(string[] failedRequirements) => new PolicyNotMetProblem(failedRequirements); + public static PolicyNotMetProblem PolicyNotMet(IEnumerable failedRequirements) => new PolicyNotMetProblem(failedRequirements); } \ No newline at end of file diff --git a/Common/Problems/CustomProblems/PolicyNotMetProblem.cs b/Common/Problems/CustomProblems/PolicyNotMetProblem.cs index 7f989c55..9345c644 100644 --- a/Common/Problems/CustomProblems/PolicyNotMetProblem.cs +++ b/Common/Problems/CustomProblems/PolicyNotMetProblem.cs @@ -1,16 +1,15 @@ using System.Net; -using Microsoft.AspNetCore.Authorization; namespace OpenShock.Common.Problems.CustomProblems; public class PolicyNotMetProblem : OpenShockProblem { - public PolicyNotMetProblem(string[] failedRequirements) : base( + public PolicyNotMetProblem(IEnumerable failedRequirements) : base( "Authorization.Policy.NotMet", "One or multiple policies were not met", HttpStatusCode.Forbidden, string.Empty) { FailedRequirements = failedRequirements; } - public string[] FailedRequirements { get; set; } + public IEnumerable FailedRequirements { get; set; } } \ No newline at end of file diff --git a/Common/Problems/CustomProblems/TokenPermissionProblem.cs b/Common/Problems/CustomProblems/TokenPermissionProblem.cs index bfdcc96d..d23b0598 100644 --- a/Common/Problems/CustomProblems/TokenPermissionProblem.cs +++ b/Common/Problems/CustomProblems/TokenPermissionProblem.cs @@ -7,11 +7,11 @@ public sealed class TokenPermissionProblem( string type, string title, PermissionType requiredPermission, - List grantedPermissions, + IEnumerable grantedPermissions, HttpStatusCode status = HttpStatusCode.BadRequest, string? detail = null) : OpenShockProblem(type, title, status, detail) { public PermissionType RequiredPermission { get; set; } = requiredPermission; - public List GrantedPermissions { get; set; } = grantedPermissions; + public IEnumerable GrantedPermissions { get; set; } = grantedPermissions; } \ No newline at end of file diff --git a/Common/Utils/TrustedProxiesFetcher.cs b/Common/Utils/TrustedProxiesFetcher.cs index 339fdc76..1097ec57 100644 --- a/Common/Utils/TrustedProxiesFetcher.cs +++ b/Common/Utils/TrustedProxiesFetcher.cs @@ -58,7 +58,7 @@ public static class TrustedProxiesFetcher private static readonly char[] NewLineSeperators = [' ', '\r', '\n', '\t']; - private static async Task> FetchCloudflareIPs(Uri uri, CancellationToken ct) + private static async Task> FetchCloudflareIPs(Uri uri, CancellationToken ct) { using var response = await Client.GetAsync(uri, ct); var stringResponse = await response.Content.ReadAsStringAsync(ct); @@ -66,7 +66,7 @@ private static async Task> FetchCloudflareIPs(Uri uri, Cancellat return ParseNetworks(stringResponse.AsSpan()); } - private static List ParseNetworks(ReadOnlySpan response) + private static IReadOnlyList ParseNetworks(ReadOnlySpan response) { var ranges = response.Split(NewLineSeperators); @@ -80,7 +80,7 @@ private static List ParseNetworks(ReadOnlySpan response) return networks; } - private static async Task FetchCloudflareIPs() + private static async Task> FetchCloudflareIPs() { try { @@ -100,14 +100,14 @@ private static async Task FetchCloudflareIPs() } } - public static async Task GetTrustedNetworksAsync(bool fetch = true) + public static async Task> GetTrustedNetworksAsync(bool fetch = true) { var cfProxies = fetch ? await FetchCloudflareIPs() : PrefetchedCloudflareProxies; return [.. PrivateNetworksParsed, .. cfProxies]; } - public static IPNetwork[] GetTrustedNetworks(bool fetch = true) + public static IReadOnlyList GetTrustedNetworks(bool fetch = true) { return GetTrustedNetworksAsync(fetch).Result; } From 2878248e528e7c8382d080eb4cfd0823dc351276 Mon Sep 17 00:00:00 2001 From: LucHeart Date: Sun, 9 Mar 2025 17:47:41 +0100 Subject: [PATCH 7/9] Friendship ended with array, we now like IReadOnlyList --- API/Controller/Shares/V2Requests.cs | 6 +++--- API/Controller/Shockers/ControlShockerController.cs | 6 ++---- Common/DeviceControl/ControlLogic.cs | 6 +++--- Common/Errors/ShareError.cs | 2 +- Common/Hubs/ShareLinkHub.cs | 4 ++-- Common/Hubs/UserHub.cs | 6 +++--- Common/Models/ControlRequest.cs | 2 +- Common/Models/Paginated.cs | 2 +- Common/Models/PermissionType.cs | 10 +++++----- .../Problems/CustomProblems/ShockersNotFoundProblem.cs | 4 ++-- Common/Services/Session/ISessionService.cs | 2 +- Common/Services/Session/SessionService.cs | 2 +- LiveControlGateway/LifetimeManager/HubLifetime.cs | 2 +- .../LifetimeManager/HubLifetimeManager.cs | 2 +- 14 files changed, 27 insertions(+), 29 deletions(-) diff --git a/API/Controller/Shares/V2Requests.cs b/API/Controller/Shares/V2Requests.cs index 45177239..4bfc5682 100644 --- a/API/Controller/Shares/V2Requests.cs +++ b/API/Controller/Shares/V2Requests.cs @@ -88,7 +88,7 @@ public IAsyncEnumerable GetIncomingRequestsList() public async Task GetRequest(Guid id) { var outstandingShare = await _db.ShareRequests.Where(x => x.Id == id && (x.Owner == CurrentUser.Id || x.User == CurrentUser.Id)) - .Select(x => new ShareRequestBaseDetails() + .Select(x => new ShareRequestBaseDetails { Id = x.Id, CreatedOn = x.CreatedOn, @@ -121,7 +121,7 @@ public async Task GetRequest(Guid id) Vibrate = y.PermVibrate, Live = y.PermLive } - }).ToArray() + }) }).FirstOrDefaultAsync(); if (outstandingShare == null) return Problem(ShareError.ShareRequestNotFound); @@ -199,5 +199,5 @@ public sealed class ShareRequestCounts public sealed class ShareRequestBaseDetails : ShareRequestBase { - public required ShockerPermLimitPairWithId[] Shockers { get; set; } + public required IEnumerable Shockers { get; set; } } \ No newline at end of file diff --git a/API/Controller/Shockers/ControlShockerController.cs b/API/Controller/Shockers/ControlShockerController.cs index 3e02cd50..0fc70285 100644 --- a/API/Controller/Shockers/ControlShockerController.cs +++ b/API/Controller/Shockers/ControlShockerController.cs @@ -1,5 +1,4 @@ -using System.Net; -using System.Net.Mime; +using System.Net.Mime; using Asp.Versioning; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; @@ -11,7 +10,6 @@ using OpenShock.Common.Models; using OpenShock.Common.Problems; using OpenShock.Common.Services.RedisPubSub; -using OpenShock.Common.Utils; namespace OpenShock.API.Controller.Shockers; @@ -65,7 +63,7 @@ public async Task SendControl( [ProducesResponseType(StatusCodes.Status412PreconditionFailed, MediaTypeNames.Application.ProblemJson)] // Shocker is paused [ProducesResponseType(StatusCodes.Status403Forbidden, MediaTypeNames.Application.ProblemJson)] // You don't have permission to control this shocker public Task SendControl_DEPRECATED( - [FromBody] Common.Models.WebSocket.User.Control[] body, + [FromBody] IReadOnlyList body, [FromServices] IHubContext userHub, [FromServices] IRedisPubService redisPubService) { diff --git a/Common/DeviceControl/ControlLogic.cs b/Common/DeviceControl/ControlLogic.cs index 6e65a79d..d7ca09ae 100644 --- a/Common/DeviceControl/ControlLogic.cs +++ b/Common/DeviceControl/ControlLogic.cs @@ -15,7 +15,7 @@ namespace OpenShock.Common.DeviceControl; public static class ControlLogic { - public static async Task> ControlByUser(Control[] shocks, OpenShockContext db, ControlLogSender sender, + public static async Task> ControlByUser(IReadOnlyList shocks, OpenShockContext db, ControlLogSender sender, IHubClients hubClients, IRedisPubService redisPubService) { var ownShockers = await db.Shockers.Where(x => x.DeviceNavigation.Owner == sender.Id).Select(x => @@ -56,7 +56,7 @@ public static async Task> ControlShareLink(Control[] shocks, OpenShockContext db, + public static async Task> ControlShareLink(IReadOnlyList shocks, OpenShockContext db, ControlLogSender sender, IHubClients hubClients, Guid shareLinkId, IRedisPubService redisPubService) { @@ -83,7 +83,7 @@ public static async Task> ControlInternal(Control[] shocks, OpenShockContext db, ControlLogSender sender, + private static async Task> ControlInternal(IReadOnlyList shocks, OpenShockContext db, ControlLogSender sender, IHubClients hubClients, IReadOnlyCollection allowedShockers, IRedisPubService redisPubService) { var finalMessages = new Dictionary>(); diff --git a/Common/Errors/ShareError.cs b/Common/Errors/ShareError.cs index 5ff07cef..2bac21c0 100644 --- a/Common/Errors/ShareError.cs +++ b/Common/Errors/ShareError.cs @@ -8,7 +8,7 @@ public static class ShareError { public static OpenShockProblem ShareRequestNotFound => new("Share.Request.NotFound", "Share request not found", HttpStatusCode.NotFound); public static OpenShockProblem ShareRequestCreateCannotShareWithSelf => new("Share.Request.Create.CannotShareWithSelf", "You cannot share something with yourself", HttpStatusCode.BadRequest); - public static ShockersNotFoundProblem ShareCreateShockerNotFound(Guid[] missingShockers) => new("Share.Request.Create.ShockerNotFound", "One or multiple of the provided shocker's were not found or do not belong to you", missingShockers, HttpStatusCode.NotFound); + public static ShockersNotFoundProblem ShareCreateShockerNotFound(IReadOnlyList missingShockers) => new("Share.Request.Create.ShockerNotFound", "One or multiple of the provided shocker's were not found or do not belong to you", missingShockers, HttpStatusCode.NotFound); public static OpenShockProblem ShareGetNoShares => new("Share.Get.NoShares", "You have no shares with the specified user, or the user doesnt exist", HttpStatusCode.NotFound); } \ No newline at end of file diff --git a/Common/Hubs/ShareLinkHub.cs b/Common/Hubs/ShareLinkHub.cs index 10e0e0d2..7778aa31 100644 --- a/Common/Hubs/ShareLinkHub.cs +++ b/Common/Hubs/ShareLinkHub.cs @@ -21,7 +21,7 @@ public sealed class ShareLinkHub : Hub private readonly ILogger _logger; private readonly IRedisPubService _redisPubService; private readonly IUserReferenceService _userReferenceService; - private IReadOnlyCollection? _tokenPermissions = null; + private IReadOnlyList? _tokenPermissions = null; public ShareLinkHub(OpenShockContext db, IHubContext userHub, ILogger logger, IRedisConnectionProvider provider, IRedisPubService redisPubService, IUserReferenceService userReferenceService) @@ -112,7 +112,7 @@ public override async Task OnConnectedAsync() await Clients.Caller.Welcome(user != null ? AuthType.Authenticated : AuthType.Guest); } - public Task Control(Models.WebSocket.User.Control[] shocks) + public Task Control(IReadOnlyList shocks) { if (!_tokenPermissions.IsAllowedAllowOnNull(PermissionType.Shockers_Use)) return Task.CompletedTask; diff --git a/Common/Hubs/UserHub.cs b/Common/Hubs/UserHub.cs index c0b573d9..2241ba37 100644 --- a/Common/Hubs/UserHub.cs +++ b/Common/Hubs/UserHub.cs @@ -25,7 +25,7 @@ public sealed class UserHub : Hub private readonly IRedisConnectionProvider _provider; private readonly IRedisPubService _redisPubService; private readonly IUserReferenceService _userReferenceService; - private IReadOnlyCollection? _tokenPermissions = null; + private IReadOnlyList? _tokenPermissions = null; public UserHub(ILogger logger, OpenShockContext db, IRedisConnectionProvider provider, IRedisPubService redisPubService, IUserReferenceService userReferenceService) @@ -69,12 +69,12 @@ public override async Task OnConnectedAsync() await Clients.Caller.DeviceStatus(final); } - public Task Control(Models.WebSocket.User.Control[] shocks) + public Task Control(IReadOnlyList shocks) { return ControlV2(shocks, null); } - public async Task ControlV2(Models.WebSocket.User.Control[] shocks, string? customName) + public async Task ControlV2(IReadOnlyList shocks, string? customName) { if (!_tokenPermissions.IsAllowedAllowOnNull(PermissionType.Shockers_Use)) return; diff --git a/Common/Models/ControlRequest.cs b/Common/Models/ControlRequest.cs index 1e88a902..e8ed208f 100644 --- a/Common/Models/ControlRequest.cs +++ b/Common/Models/ControlRequest.cs @@ -2,6 +2,6 @@ public sealed class ControlRequest { - public required WebSocket.User.Control[] Shocks { get; set; } + public required IReadOnlyList Shocks { get; set; } public string? CustomName { get; set; } = null; } \ No newline at end of file diff --git a/Common/Models/Paginated.cs b/Common/Models/Paginated.cs index be8bab48..4489e1f6 100644 --- a/Common/Models/Paginated.cs +++ b/Common/Models/Paginated.cs @@ -5,5 +5,5 @@ public sealed class Paginated public required int Offset { get; set; } public required int Limit { get; set; } public required long Total { get; set; } - public required T[] Data { get; set; } + public required IReadOnlyList Data { get; set; } } \ No newline at end of file diff --git a/Common/Models/PermissionType.cs b/Common/Models/PermissionType.cs index 040dd85c..541cd7e8 100644 --- a/Common/Models/PermissionType.cs +++ b/Common/Models/PermissionType.cs @@ -23,16 +23,16 @@ public enum PermissionType public static class PermissionTypeExtensions { - public static bool IsAllowed(this PermissionType permissionType, IReadOnlyCollection permissions) => + public static bool IsAllowed(this PermissionType permissionType, IReadOnlyList permissions) => IsAllowedInternal(permissions, permissionType); - public static bool IsAllowed(this IReadOnlyCollection permissions, PermissionType permissionType) => + public static bool IsAllowed(this IReadOnlyList permissions, PermissionType permissionType) => IsAllowedInternal(permissions, permissionType); - public static bool IsAllowedAllowOnNull(this IReadOnlyCollection? permissions, + public static bool IsAllowedAllowOnNull(this IReadOnlyList? permissions, PermissionType permissionType) => permissions == null || IsAllowedInternal(permissions, permissionType); - private static bool IsAllowedInternal(IReadOnlyCollection permissions, PermissionType permissionType) + private static bool IsAllowedInternal(IReadOnlyList permissions, PermissionType permissionType) { // ReSharper disable once PossibleMultipleEnumeration return permissions.Contains(permissionType) || permissions.Any(x => @@ -85,4 +85,4 @@ private static PermissionTypeRecord[] GetBindings() } } -public record PermissionTypeRecord(PermissionType PermissionType, string Name, ICollection Parents); \ No newline at end of file +public record PermissionTypeRecord(PermissionType PermissionType, string Name, IReadOnlyList Parents); \ No newline at end of file diff --git a/Common/Problems/CustomProblems/ShockersNotFoundProblem.cs b/Common/Problems/CustomProblems/ShockersNotFoundProblem.cs index bddded97..29ed27fd 100644 --- a/Common/Problems/CustomProblems/ShockersNotFoundProblem.cs +++ b/Common/Problems/CustomProblems/ShockersNotFoundProblem.cs @@ -5,10 +5,10 @@ namespace OpenShock.Common.Problems.CustomProblems; public sealed class ShockersNotFoundProblem( string type, string title, - Guid[] missingShockers, + IReadOnlyList missingShockers, HttpStatusCode status = HttpStatusCode.BadRequest, string? detail = null) : OpenShockProblem(type, title, status, detail) { - public Guid[] MissingShockers { get; set; } = missingShockers; + public IReadOnlyList MissingShockers { get; set; } = missingShockers; } \ No newline at end of file diff --git a/Common/Services/Session/ISessionService.cs b/Common/Services/Session/ISessionService.cs index 9d0db7ee..7fedb224 100644 --- a/Common/Services/Session/ISessionService.cs +++ b/Common/Services/Session/ISessionService.cs @@ -6,7 +6,7 @@ public interface ISessionService { public Task CreateSessionAsync(string sessionId, Guid userId, string userAgent, string ipAddress); - public Task ListSessionsByUserId(Guid userId); + public Task> ListSessionsByUserId(Guid userId); public Task GetSessionById(string sessionId); diff --git a/Common/Services/Session/SessionService.cs b/Common/Services/Session/SessionService.cs index ffefcd5d..a2c2086c 100644 --- a/Common/Services/Session/SessionService.cs +++ b/Common/Services/Session/SessionService.cs @@ -42,7 +42,7 @@ await _loginSessions.InsertAsync(new LoginSession return publicId; } - public async Task ListSessionsByUserId(Guid userId) + public async Task> ListSessionsByUserId(Guid userId) { var sessions = await _loginSessions.Where(x => x.UserId == userId).ToArrayAsync(); diff --git a/LiveControlGateway/LifetimeManager/HubLifetime.cs b/LiveControlGateway/LifetimeManager/HubLifetime.cs index cec8bf09..85397ff7 100644 --- a/LiveControlGateway/LifetimeManager/HubLifetime.cs +++ b/LiveControlGateway/LifetimeManager/HubLifetime.cs @@ -260,7 +260,7 @@ private static DateTimeOffset CalculateActiveUntil(byte tps) => /// /// /// - public ValueTask Control(IList shocks) + public ValueTask Control(IReadOnlyList shocks) { var shocksTransformed = new List(); foreach (var shock in shocks) diff --git a/LiveControlGateway/LifetimeManager/HubLifetimeManager.cs b/LiveControlGateway/LifetimeManager/HubLifetimeManager.cs index 3f2b370d..6332cb7a 100644 --- a/LiveControlGateway/LifetimeManager/HubLifetimeManager.cs +++ b/LiveControlGateway/LifetimeManager/HubLifetimeManager.cs @@ -214,7 +214,7 @@ public async Task> UpdateDevice(Guid device) /// /// public async Task> Control(Guid device, - IList shocks) + IReadOnlyList shocks) { if (!_lifetimes.TryGetValue(device, out var deviceLifetime)) return new DeviceNotFound(); await deviceLifetime.Control(shocks); From 4737cae06b3103e96e923bbdee053daa7289ee54 Mon Sep 17 00:00:00 2001 From: LucHeart Date: Sun, 9 Mar 2025 17:56:58 +0100 Subject: [PATCH 8/9] More --- Common/DeviceControl/ControlLogic.cs | 6 ++++-- Common/Redis/PubSub/ControlMessage.cs | 2 +- Common/Services/RedisPubSub/IRedisPubService.cs | 5 +++-- Common/Services/RedisPubSub/RedisPubService.cs | 2 +- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Common/DeviceControl/ControlLogic.cs b/Common/DeviceControl/ControlLogic.cs index d7ca09ae..049f1d8d 100644 --- a/Common/DeviceControl/ControlLogic.cs +++ b/Common/DeviceControl/ControlLogic.cs @@ -86,7 +86,7 @@ public static async Task> ControlInternal(IReadOnlyList shocks, OpenShockContext db, ControlLogSender sender, IHubClients hubClients, IReadOnlyCollection allowedShockers, IRedisPubService redisPubService) { - var finalMessages = new Dictionary>(); + var finalMessages = new Dictionary>(); var curTime = DateTime.UtcNow; var distinctShocks = shocks.DistinctBy(x => x.Id); var logs = new Dictionary>(); @@ -147,7 +147,9 @@ private static async Task kvp.Key, IReadOnlyList (kvp) => kvp.Value)); + var logSends = logs.Select(x => hubClients.User(x.Key.ToString()).Log(sender, x.Value)); await Task.WhenAll([ diff --git a/Common/Redis/PubSub/ControlMessage.cs b/Common/Redis/PubSub/ControlMessage.cs index 2c84144a..5a7ce145 100644 --- a/Common/Redis/PubSub/ControlMessage.cs +++ b/Common/Redis/PubSub/ControlMessage.cs @@ -11,7 +11,7 @@ public sealed class ControlMessage /// /// Guid is the device id /// - public required IDictionary> ControlMessages { get; set; } + public required IDictionary> ControlMessages { get; set; } public sealed class ShockerControlInfo { diff --git a/Common/Services/RedisPubSub/IRedisPubService.cs b/Common/Services/RedisPubSub/IRedisPubService.cs index 96738908..17baaebf 100644 --- a/Common/Services/RedisPubSub/IRedisPubService.cs +++ b/Common/Services/RedisPubSub/IRedisPubService.cs @@ -11,14 +11,15 @@ public interface IRedisPubService /// /// public Task SendDeviceOnlineStatus(Guid deviceId); - + /// /// General shocker control /// /// /// /// - Task SendDeviceControl(Guid sender, IDictionary> controlMessages); + Task SendDeviceControl(Guid sender, + IDictionary> controlMessages); /// /// Toggle captive portal diff --git a/Common/Services/RedisPubSub/RedisPubService.cs b/Common/Services/RedisPubSub/RedisPubService.cs index f99f05fa..cc1a0539 100644 --- a/Common/Services/RedisPubSub/RedisPubService.cs +++ b/Common/Services/RedisPubSub/RedisPubService.cs @@ -29,7 +29,7 @@ public Task SendDeviceOnlineStatus(Guid deviceId) } /// - public Task SendDeviceControl(Guid sender, IDictionary> controlMessages) + public Task SendDeviceControl(Guid sender, IDictionary> controlMessages) { var redisMessage = new ControlMessage { From fcfda2e0610f89a5da72d8d106acfb539d3a0a66 Mon Sep 17 00:00:00 2001 From: HeavenVR Date: Mon, 10 Mar 2025 11:48:21 +0100 Subject: [PATCH 9/9] Remove async keyword on endpoints returning IAsyncEnumerable --- API/Controller/Devices/DevicesController.cs | 2 +- API/Controller/Shockers/OwnShockerController.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/API/Controller/Devices/DevicesController.cs b/API/Controller/Devices/DevicesController.cs index bbbaaf5c..ce514d79 100644 --- a/API/Controller/Devices/DevicesController.cs +++ b/API/Controller/Devices/DevicesController.cs @@ -25,7 +25,7 @@ public sealed partial class DevicesController [HttpGet] [ProducesResponseType>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] [MapToApiVersion("1")] - public async Task ListDevices() + public IActionResult ListDevices() { var devices = _db.Devices .Where(x => x.Owner == CurrentUser.Id) diff --git a/API/Controller/Shockers/OwnShockerController.cs b/API/Controller/Shockers/OwnShockerController.cs index b295a3cd..6721007e 100644 --- a/API/Controller/Shockers/OwnShockerController.cs +++ b/API/Controller/Shockers/OwnShockerController.cs @@ -17,7 +17,7 @@ public sealed partial class ShockerController [HttpGet("own")] [ProducesResponseType>>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] [MapToApiVersion("1")] - public async Task ListShockers() + public IActionResult ListShockers() { var shockers = _db.Devices .Where(x => x.Owner == CurrentUser.Id)