diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index d3a39976..a73b2ad3 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -33,7 +33,7 @@ name: ci-build env: REGISTRY: ghcr.io - DOTNET_VERSION: 9.0.x + DOTNET_VERSION: 10.0.x jobs: @@ -60,7 +60,7 @@ jobs: - name: Run ${{ matrix.name }} tests run: | set -euo pipefail - dotnet test -c Release ${{ matrix.project }} + dotnet test -c Release --project ${{ matrix.project }} build: name: Build (${{ matrix.image }}) diff --git a/.github/workflows/ci-tag.yml b/.github/workflows/ci-tag.yml index 504ccb81..959050ce 100644 --- a/.github/workflows/ci-tag.yml +++ b/.github/workflows/ci-tag.yml @@ -7,7 +7,7 @@ on: name: ci-tag env: - DOTNET_VERSION: 9.0.x + DOTNET_VERSION: 10.0.x REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository_owner }}/api diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 40fe5f3f..d9cbb591 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -9,7 +9,7 @@ on: - cron: '0 6 * * 1' env: - DOTNET_VERSION: 9.x.x + DOTNET_VERSION: 10.x.x jobs: analyze: diff --git a/API/Controller/Admin/GetOnlineDevices.cs b/API/Controller/Admin/GetOnlineDevices.cs index da8ef99b..b7615ea7 100644 --- a/API/Controller/Admin/GetOnlineDevices.cs +++ b/API/Controller/Admin/GetOnlineDevices.cs @@ -22,7 +22,7 @@ public async Task GetOnlineDevices() { var devicesOnline = _redis.RedisCollection(false); - var allOnlineDevices = await devicesOnline.ToArrayAsync(); + var allOnlineDevices = await devicesOnline.ToListAsync(); var dbLookup = await _db.Devices .Where(x => allOnlineDevices.Select(y => y.Id).Contains(x.Id)) .Select(x => new diff --git a/API/Controller/Admin/_ApiController.cs b/API/Controller/Admin/_ApiController.cs index 6bcb2d71..134ce0d5 100644 --- a/API/Controller/Admin/_ApiController.cs +++ b/API/Controller/Admin/_ApiController.cs @@ -9,6 +9,7 @@ namespace OpenShock.API.Controller.Admin; [ApiController] [Tags("Admin")] +[EndpointGroupName("admin")] [Route("/{version:apiVersion}/admin")] [Authorize(AuthenticationSchemes = OpenShockAuthSchemes.UserSessionCookie, Roles = "Admin")] public sealed partial class AdminController : AuthenticatedSessionControllerBase diff --git a/API/Controller/OAuth/Authorize.cs b/API/Controller/OAuth/Authorize.cs index 05de0fdd..60ebf430 100644 --- a/API/Controller/OAuth/Authorize.cs +++ b/API/Controller/OAuth/Authorize.cs @@ -20,7 +20,6 @@ public sealed partial class OAuthController /// Unsupported or misconfigured provider. [EnableRateLimiting("auth")] [HttpPost("{provider}/authorize")] - [ApiExplorerSettings(IgnoreApi = true)] public async Task OAuthAuthorize([FromRoute] string provider, [FromQuery(Name="flow"), Required] OAuthFlow flow) { if (!await _schemeProvider.IsSupportedOAuthScheme(provider)) diff --git a/API/Controller/OAuth/HandOff.cs b/API/Controller/OAuth/HandOff.cs index 6d673f85..7c3e20ad 100644 --- a/API/Controller/OAuth/HandOff.cs +++ b/API/Controller/OAuth/HandOff.cs @@ -21,7 +21,6 @@ public sealed partial class OAuthController /// [EnableRateLimiting("auth")] [HttpGet("{provider}/handoff")] - [ApiExplorerSettings(IgnoreApi = true)] public async Task OAuthHandOff( [FromRoute] string provider, [FromServices] IOAuthConnectionService connectionService, diff --git a/API/Controller/OAuth/SignupFinalize.cs b/API/Controller/OAuth/SignupFinalize.cs index 4f0d5ef4..c5dc2127 100644 --- a/API/Controller/OAuth/SignupFinalize.cs +++ b/API/Controller/OAuth/SignupFinalize.cs @@ -26,7 +26,6 @@ public sealed partial class OAuthController /// [EnableRateLimiting("auth")] [HttpPost("{provider}/signup-finalize")] - [ApiExplorerSettings(IgnoreApi = true)] public async Task OAuthSignupFinalize( [FromRoute] string provider, [FromBody] OAuthFinalizeRequest body, diff --git a/API/Controller/OAuth/SignupGetData.cs b/API/Controller/OAuth/SignupGetData.cs index 0bfc3b47..3abfcb70 100644 --- a/API/Controller/OAuth/SignupGetData.cs +++ b/API/Controller/OAuth/SignupGetData.cs @@ -22,7 +22,6 @@ public sealed partial class OAuthController [ResponseCache(NoStore = true)] [EnableRateLimiting("auth")] [HttpGet("{provider}/signup-data")] - [ApiExplorerSettings(IgnoreApi = true)] public async Task OAuthSignupGetData([FromRoute] string provider) { if (User.HasOpenShockUserIdentity()) diff --git a/API/Controller/OAuth/_ApiController.cs b/API/Controller/OAuth/_ApiController.cs index 05486d31..4251a97b 100644 --- a/API/Controller/OAuth/_ApiController.cs +++ b/API/Controller/OAuth/_ApiController.cs @@ -13,9 +13,8 @@ namespace OpenShock.API.Controller.OAuth; /// OAuth management endpoints (provider listing, authorize, data handoff). /// [ApiController] -[Tags("OAuth")] -[ApiVersion("1")] -[Route("/{version:apiVersion}/oauth")] +[EndpointGroupName("oauth")] +[Route("/{version:apiVersion}/oauth"), ApiVersion("1")] public sealed partial class OAuthController : OpenShockControllerBase { private readonly IAccountService _accountService; diff --git a/API/Controller/Sessions/ListSessions.cs b/API/Controller/Sessions/ListSessions.cs index 5762c110..964b1c26 100644 --- a/API/Controller/Sessions/ListSessions.cs +++ b/API/Controller/Sessions/ListSessions.cs @@ -1,5 +1,4 @@ using System.Net.Mime; -using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Mvc; using OpenShock.API.Models.Response; @@ -9,11 +8,9 @@ public sealed partial class SessionsController { [HttpGet] [ProducesResponseType(StatusCodes.Status200OK, MediaTypeNames.Application.Json)] - public async IAsyncEnumerable ListSessions([EnumeratorCancellation] CancellationToken cancellationToken) + public IAsyncEnumerable ListSessions() { - await foreach (var session in _sessionService.ListSessionsByUserIdAsync(CurrentUser.Id).WithCancellation(cancellationToken)) - { - yield return LoginSessionResponse.MapFrom(session); - } + return _sessionService.ListSessionsByUserIdAsync(CurrentUser.Id) + .Select(LoginSessionResponse.MapFrom); } } \ No newline at end of file diff --git a/API/Program.cs b/API/Program.cs index 014c094e..c5f707df 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -11,11 +11,11 @@ using OpenShock.Common; using OpenShock.Common.Extensions; using OpenShock.Common.Hubs; +using OpenShock.Common.OpenAPI; using OpenShock.Common.Services; using OpenShock.Common.Services.Device; using OpenShock.Common.Services.LCGNodeProvisioner; using OpenShock.Common.Services.Ota; -using OpenShock.Common.Swagger; using Serilog; using OAuthConstants = OpenShock.API.OAuth.OAuthConstants; @@ -103,7 +103,7 @@ static void DefaultOptions(RemoteAuthenticationOptions options, string provider) builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.AddSwaggerExt(); +builder.AddOpenApiExt(); builder.AddCloudflareTurnstileService(); builder.AddEmailService(); diff --git a/Common/Common.csproj b/Common/Common.csproj index 8c6c3174..accb64d8 100644 --- a/Common/Common.csproj +++ b/Common/Common.csproj @@ -13,6 +13,7 @@ + @@ -31,7 +32,6 @@ - diff --git a/Common/DataAnnotations/EmailAddressAttribute.cs b/Common/DataAnnotations/EmailAddressAttribute.cs index cf7a3ec0..475a813a 100644 --- a/Common/DataAnnotations/EmailAddressAttribute.cs +++ b/Common/DataAnnotations/EmailAddressAttribute.cs @@ -1,9 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.Net.Mail; -using Microsoft.OpenApi.Any; -using Microsoft.OpenApi.Models; using OpenShock.Common.Constants; -using OpenShock.Common.DataAnnotations.Interfaces; namespace OpenShock.Common.DataAnnotations; @@ -13,8 +10,8 @@ namespace OpenShock.Common.DataAnnotations; /// /// Inherits from . /// -[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] -public sealed class EmailAddressAttribute : ValidationAttribute, IParameterAttribute +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)] +public sealed class EmailAddressAttribute : ValidationAttribute { /// /// Example value used to generate OpenApi documentation. @@ -55,15 +52,4 @@ public sealed class EmailAddressAttribute : ValidationAttribute, IParameterAttri return ValidationResult.Success; } - - /// - public void Apply(OpenApiSchema schema) - { - //if (ShouldValidate) schema.Pattern = ???; - - schema.Example = new OpenApiString(ExampleValue); - } - - /// - public void Apply(OpenApiParameter parameter) => Apply(parameter.Schema); } \ No newline at end of file diff --git a/Common/DataAnnotations/Interfaces/IOperationAttribute.cs b/Common/DataAnnotations/Interfaces/IOperationAttribute.cs deleted file mode 100644 index d47bd156..00000000 --- a/Common/DataAnnotations/Interfaces/IOperationAttribute.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.OpenApi.Models; - -namespace OpenShock.Common.DataAnnotations.Interfaces; - -/// -/// Represents an interface for operation attributes that can be applied to an OpenApiOperation instance. -/// -public interface IOperationAttribute -{ - /// - /// Applies the operation attribute to the given OpenApiOperation instance. - /// - /// The OpenApiOperation instance to apply the attribute to. - void Apply(OpenApiOperation operation); -} \ No newline at end of file diff --git a/Common/DataAnnotations/Interfaces/IParameterAttribute.cs b/Common/DataAnnotations/Interfaces/IParameterAttribute.cs deleted file mode 100644 index 295f4d5e..00000000 --- a/Common/DataAnnotations/Interfaces/IParameterAttribute.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.OpenApi.Models; - -namespace OpenShock.Common.DataAnnotations.Interfaces; - -/// -/// Represents an interface for parameter attributes that can be applied to an OpenApiSchema or OpenApiParameter instance. -/// -public interface IParameterAttribute -{ - /// - /// Applies the parameter attribute to the given OpenApiSchema instance. - /// - /// The OpenApiSchema instance to apply the attribute to. - void Apply(OpenApiSchema schema); - - /// - /// Applies the parameter attribute to the given OpenApiParameter instance. - /// - /// The OpenApiParameter instance to apply the attribute to. - void Apply(OpenApiParameter parameter); -} \ No newline at end of file diff --git a/Common/DataAnnotations/OpenApiSchemas.cs b/Common/DataAnnotations/OpenApiSchemas.cs deleted file mode 100644 index 69651381..00000000 --- a/Common/DataAnnotations/OpenApiSchemas.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.OpenApi.Any; -using Microsoft.OpenApi.Models; -using OpenShock.Common.Models; - -namespace OpenShock.Common.DataAnnotations; - -public static class OpenApiSchemas -{ - public static OpenApiSchema SemVerSchema => new OpenApiSchema { - Title = "SemVer", - Type = "string", - Pattern = /* lang=regex */ "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", - Example = new OpenApiString("1.0.0-dev+a16f2") - }; - - public static OpenApiSchema PauseReasonEnumSchema => new OpenApiSchema { - Title = nameof(PauseReason), - Type = "integer", - Description = """ - An integer representing the reason(s) for the shocker being paused, expressed as a bitfield where reasons are OR'd together. - - Each bit corresponds to: - - 1: Shocker - - 2: UserShare - - 4: PublicShare - - For example, a value of 6 (2 | 4) indicates both 'UserShare' and 'PublicShare' reasons. - """, - Example = new OpenApiInteger(6) - }; -} diff --git a/Common/DataAnnotations/PasswordAttribute.cs b/Common/DataAnnotations/PasswordAttribute.cs index ca42b60a..b6dca5bd 100644 --- a/Common/DataAnnotations/PasswordAttribute.cs +++ b/Common/DataAnnotations/PasswordAttribute.cs @@ -1,8 +1,5 @@ using System.ComponentModel.DataAnnotations; -using Microsoft.OpenApi.Any; -using Microsoft.OpenApi.Models; using OpenShock.Common.Constants; -using OpenShock.Common.DataAnnotations.Interfaces; namespace OpenShock.Common.DataAnnotations; @@ -12,8 +9,8 @@ namespace OpenShock.Common.DataAnnotations; /// /// Inherits from . /// -[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] -public sealed class PasswordAttribute : ValidationAttribute, IParameterAttribute +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)] +public sealed class PasswordAttribute : ValidationAttribute { /// /// Example value used to generate OpenApi documentation. @@ -54,15 +51,4 @@ public sealed class PasswordAttribute : ValidationAttribute, IParameterAttribute return ValidationResult.Success; } - - /// - public void Apply(OpenApiSchema schema) - { - //if (ShouldValidate) schema.Pattern = ???; - - schema.Example = new OpenApiString(ExampleValue); - } - - /// - public void Apply(OpenApiParameter parameter) => Apply(parameter.Schema); } \ No newline at end of file diff --git a/Common/DataAnnotations/StringCollectionItemMaxLengthAttribute.cs b/Common/DataAnnotations/StringCollectionItemMaxLengthAttribute.cs index 08cb6eea..55fd7113 100644 --- a/Common/DataAnnotations/StringCollectionItemMaxLengthAttribute.cs +++ b/Common/DataAnnotations/StringCollectionItemMaxLengthAttribute.cs @@ -2,7 +2,7 @@ namespace OpenShock.Common.DataAnnotations; -[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = true)] +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] public sealed class StringCollectionItemMaxLengthAttribute : ValidationAttribute { public StringCollectionItemMaxLengthAttribute(int maxLength) diff --git a/Common/DataAnnotations/UsernameAttribute.cs b/Common/DataAnnotations/UsernameAttribute.cs index c0b366d7..03572ed2 100644 --- a/Common/DataAnnotations/UsernameAttribute.cs +++ b/Common/DataAnnotations/UsernameAttribute.cs @@ -1,7 +1,4 @@ using System.ComponentModel.DataAnnotations; -using Microsoft.OpenApi.Any; -using Microsoft.OpenApi.Models; -using OpenShock.Common.DataAnnotations.Interfaces; using OpenShock.Common.Validation; namespace OpenShock.Common.DataAnnotations; @@ -12,8 +9,8 @@ namespace OpenShock.Common.DataAnnotations; /// /// Inherits from . /// -[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)] -public sealed class UsernameAttribute : ValidationAttribute, IParameterAttribute +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)] +public sealed class UsernameAttribute : ValidationAttribute { /// /// Example value used to generate OpenApi documentation. @@ -50,15 +47,4 @@ public sealed class UsernameAttribute : ValidationAttribute, IParameterAttribute error => new ValidationResult($"{error.Type} - {error.Message}") ); } - - /// - public void Apply(OpenApiSchema schema) - { - //if (ShouldValidate) schema.Pattern = ???; - - schema.Example = new OpenApiString(ExampleValue); - } - - /// - public void Apply(OpenApiParameter parameter) => Apply(parameter.Schema); } \ No newline at end of file diff --git a/Common/Hubs/UserHub.cs b/Common/Hubs/UserHub.cs index cd0925d1..a26be47c 100644 --- a/Common/Hubs/UserHub.cs +++ b/Common/Hubs/UserHub.cs @@ -47,7 +47,7 @@ public override async Task OnConnectedAsync() .Where(x => x.Shockers.Any(y => y.UserShares.Any(z => z.SharedWithUserId == UserId))) .Select(x => x.Id.ToString()).ToArrayAsync(); - var own = devicesOnline.Where(x => x.Owner == UserId).ToArrayAsync(); + var own = devicesOnline.Where(x => x.Owner == UserId).ToListAsync(); var shared = devicesOnline.FindByIdsAsync(sharedDevices); await Task.WhenAll(own, shared); diff --git a/Common/OpenAPI/DocumentDefaults.cs b/Common/OpenAPI/DocumentDefaults.cs new file mode 100644 index 00000000..54764ce6 --- /dev/null +++ b/Common/OpenAPI/DocumentDefaults.cs @@ -0,0 +1,73 @@ +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi; + +namespace OpenShock.Common.OpenAPI; + +public static class DocumentDefaults +{ + public static Func GetDocumentTransformer(string version) + { + return (document, context, _) => + { + var env = context.ApplicationServices.GetRequiredService(); + + document.Info = new OpenApiInfo + { + Title = "OpenShock API", + // Summary = ... + // Description = ... + Version = version, + // TermsOfService = ... + // Contact = ... + // License = ... + }; + + document.Servers = + [ + new OpenApiServer { Url = "https://api.openshock.app" }, + new OpenApiServer { Url = "https://api.openshock.dev" } + ]; + if (env.IsDevelopment()) + { + document.Servers.Add(new OpenApiServer { Url = "https://localhost" }); + } + + document.Components ??= new OpenApiComponents(); + document.Components.SecuritySchemes = new Dictionary + { + { + "ApiToken", + new OpenApiSecurityScheme + { + Name = "OpenShockToken", + Description = "Enter API Token", + Type = SecuritySchemeType.ApiKey, + In = ParameterLocation.Header + } + }, + { + "HubToken", + new OpenApiSecurityScheme + { + Name = "DeviceToken", + Description = "Enter hub token", + Type = SecuritySchemeType.ApiKey, + In = ParameterLocation.Header + } + }, + { + "UserSessionCookie", + new OpenApiSecurityScheme + { + Name = "openShockSession", + Description = "Enter user session cookie", + Type = SecuritySchemeType.ApiKey, + In = ParameterLocation.Cookie + } + } + }; + + return Task.CompletedTask; + }; + } +} \ No newline at end of file diff --git a/Common/OpenAPI/OpenApiExtensions.cs b/Common/OpenAPI/OpenApiExtensions.cs new file mode 100644 index 00000000..bed9be8b --- /dev/null +++ b/Common/OpenAPI/OpenApiExtensions.cs @@ -0,0 +1,44 @@ +using Microsoft.OpenApi; + +namespace OpenShock.Common.OpenAPI; + +public static class OpenApiExtensions +{ + public static IServiceCollection AddOpenApiExt(this WebApplicationBuilder builder) where TProgram : class + { + var assembly = typeof(TProgram).Assembly; + + string assemblyName = assembly + .GetName() + .Name ?? throw new NullReferenceException("Assembly name"); + + builder.Services.AddOutputCache(options => + { + options.AddPolicy("OpenAPI", policy => policy.Expire(TimeSpan.FromMinutes(10))); + }); + builder.Services.AddOpenApi(options => + { + options.OpenApiVersion = OpenApiSpecVersion.OpenApi3_1; + options.AddDocumentTransformer(DocumentDefaults.GetDocumentTransformer(version: "1")); + }); + builder.Services.AddOpenApi("v2", options => + { + options.OpenApiVersion = OpenApiSpecVersion.OpenApi3_1; + options.AddDocumentTransformer(DocumentDefaults.GetDocumentTransformer(version: "2")); + }); + builder.Services.AddOpenApi("oauth", options => + { + options.OpenApiVersion = OpenApiSpecVersion.OpenApi3_1; + options.ShouldInclude = apiDescription => apiDescription.GroupName is "oauth"; + options.AddDocumentTransformer(DocumentDefaults.GetDocumentTransformer(version: "1")); + }); + builder.Services.AddOpenApi("admin", options => + { + options.OpenApiVersion = OpenApiSpecVersion.OpenApi3_1; + options.ShouldInclude = apiDescription => apiDescription.GroupName is "admin"; + options.AddDocumentTransformer(DocumentDefaults.GetDocumentTransformer(version: "1")); + }); + + return builder.Services; + } +} \ No newline at end of file diff --git a/Common/OpenShockMiddlewareHelper.cs b/Common/OpenShockMiddlewareHelper.cs index 562e99e1..85b6299e 100644 --- a/Common/OpenShockMiddlewareHelper.cs +++ b/Common/OpenShockMiddlewareHelper.cs @@ -8,6 +8,7 @@ using Redis.OM.Contracts; using Scalar.AspNetCore; using Serilog; +using IPNetwork = System.Net.IPNetwork; namespace OpenShock.Common; @@ -24,11 +25,11 @@ public static class OpenShockMiddlewareHelper public static async Task UseCommonOpenShockMiddleware(this WebApplication app) { var metricsOptions = app.Services.GetRequiredService(); - var metricsAllowedIpNetworks = metricsOptions.AllowedNetworks.Select(x => IPNetwork.Parse(x)).ToArray(); + var metricsAllowedIpNetworks = metricsOptions.AllowedNetworks.Select(IPNetwork.Parse).ToArray(); foreach (var proxy in await TrustedProxiesFetcher.GetTrustedNetworksAsync()) { - ForwardedSettings.KnownNetworks.Add(proxy); + ForwardedSettings.KnownIPNetworks.Add(proxy); } app.UseForwardedHeaders(ForwardedSettings); @@ -78,7 +79,8 @@ public static async Task UseCommonOpenShockMiddleware(this return remoteIp is not null && metricsAllowedIpNetworks.Any(x => x.Contains(remoteIp)); }); - app.UseSwagger(); + app.MapOpenApi() + .CacheOutput("OpenAPI"); app.MapScalarApiReference("/scalar/viewer", options => options diff --git a/Common/Services/LCGNodeProvisioner/LCGNodeProvisioner.cs b/Common/Services/LCGNodeProvisioner/LCGNodeProvisioner.cs index 7b218763..82f513b3 100644 --- a/Common/Services/LCGNodeProvisioner/LCGNodeProvisioner.cs +++ b/Common/Services/LCGNodeProvisioner/LCGNodeProvisioner.cs @@ -43,7 +43,7 @@ public LCGNodeProvisioner(IRedisConnectionProvider redisConnectionProvider, IWeb var nodes = await _lcgNodes .Where(x => x.Environment == _environmentName) - .ToArrayAsync(); + .ToListAsync(); var node = nodes .OrderBy(x => DistanceLookup.TryGetDistanceBetween(x.Country, countryCode, out float distance) ? distance : Distance.DistanceToAndromedaGalaxyInKm) // Just a large number :3 diff --git a/Common/Services/Session/SessionService.cs b/Common/Services/Session/SessionService.cs index ab9e3da9..f506640f 100644 --- a/Common/Services/Session/SessionService.cs +++ b/Common/Services/Session/SessionService.cs @@ -95,11 +95,11 @@ public async Task DeleteSessionByIdAsync(Guid sessionId) public async Task DeleteSessionsByUserIdAsync(Guid userId) { - var sessions = await _loginSessions.Where(x => x.UserId == userId).ToArrayAsync(); + var sessions = await _loginSessions.Where(x => x.UserId == userId).ToListAsync(); await _loginSessions.DeleteAsync(sessions); - return sessions.Length; + return sessions.Count; } public async Task DeleteSessionAsync(LoginSession loginSession) diff --git a/Common/Swagger/AttributeFilter.cs b/Common/Swagger/AttributeFilter.cs deleted file mode 100644 index 8634be86..00000000 --- a/Common/Swagger/AttributeFilter.cs +++ /dev/null @@ -1,118 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.OpenApi.Models; -using OpenShock.Common.Authentication; -using OpenShock.Common.DataAnnotations.Interfaces; -using Swashbuckle.AspNetCore.SwaggerGen; - -namespace OpenShock.Common.Swagger; - -public sealed class AttributeFilter : ISchemaFilter, IParameterFilter, IOperationFilter -{ - public void Apply(OpenApiParameter parameter, ParameterFilterContext context) - { - // Apply OpenShock Parameter Attributes - foreach (var attribute in context.ParameterInfo?.GetCustomAttributes(true).OfType() ?? []) - { - attribute.Apply(parameter); - } - - // Apply OpenShock Parameter Attributes - foreach (var attribute in context.PropertyInfo?.GetCustomAttributes(true).OfType() ?? []) - { - attribute.Apply(parameter); - } - } - - public void Apply(OpenApiSchema schema, SchemaFilterContext context) - { - // Apply OpenShock Parameter Attributes - foreach (var attribute in context.MemberInfo?.GetCustomAttributes(true).OfType() ?? []) - { - attribute.Apply(schema); - } - } - - public void Apply(OpenApiOperation operation, OperationFilterContext context) - { - // Apply OpenShock Parameter Attributes - foreach (var attribute in context.MethodInfo?.GetCustomAttributes(true).OfType() ?? []) - { - attribute.Apply(operation); - } - - // Get Authorize attribute - var attributes = context.MethodInfo?.DeclaringType?.GetCustomAttributes(true) - .Union(context.MethodInfo.GetCustomAttributes(true)) - .OfType() - .ToArray() ?? []; - - if (attributes.Length != 0) - { - if (attributes.Count(attr => !string.IsNullOrEmpty(attr.AuthenticationSchemes)) > 1) throw new Exception("Dunno what to apply to this method (multiple authentication attributes with schemes set)"); - - var scheme = attributes.Select(attr => attr.AuthenticationSchemes).SingleOrDefault(scheme => !string.IsNullOrEmpty(scheme)); - var roles = attributes.Select(attr => attr.Roles).Where(roles => !string.IsNullOrEmpty(roles)).SelectMany(roles => roles!.Split(',')).Select(role => role.Trim()).ToArray(); - var policies = attributes.Select(attr => attr.Policy).Where(policies => !string.IsNullOrEmpty(policies)).SelectMany(policies => policies!.Split(',')).Select(policy => policy.Trim()).ToArray(); - - // Add what should be show inside the security section - List securityInfos = []; - if (!string.IsNullOrEmpty(scheme)) securityInfos.Add($"{nameof(AuthorizeAttribute.AuthenticationSchemes)}:{scheme}"); - if (roles.Length > 0) securityInfos.Add($"{nameof(AuthorizeAttribute.Roles)}:{string.Join(',', roles)}"); - if (policies.Length > 0) securityInfos.Add($"{nameof(AuthorizeAttribute.Policy)}:{string.Join(',', policies)}"); - - List securityRequirements = []; - foreach (var authenticationScheme in scheme?.Split(',').Select(s => s.Trim()) ?? []) - { - securityRequirements.AddRange(authenticationScheme switch - { - OpenShockAuthSchemes.UserSessionCookie => [ - new OpenApiSecurityRequirement {{ - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Id = OpenShockAuthSchemes.UserSessionCookie, - Type = ReferenceType.SecurityScheme, - } - }, - securityInfos - }} - ], - OpenShockAuthSchemes.ApiToken => [ - new OpenApiSecurityRequirement {{ - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Id = OpenShockAuthSchemes.ApiToken, - Type = ReferenceType.SecurityScheme, - } - }, - securityInfos - }} - ], - OpenShockAuthSchemes.HubToken => [ - new OpenApiSecurityRequirement {{ - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Id = OpenShockAuthSchemes.HubToken, - Type = ReferenceType.SecurityScheme - } - }, - securityInfos - }} - ], - _ => [], - }); - } - - operation.Security = securityRequirements; - } - else - { - operation.Security.Clear(); - } - } -} diff --git a/Common/Swagger/SwaggerGenExtensions.cs b/Common/Swagger/SwaggerGenExtensions.cs deleted file mode 100644 index 9eabfbc9..00000000 --- a/Common/Swagger/SwaggerGenExtensions.cs +++ /dev/null @@ -1,101 +0,0 @@ -using Microsoft.OpenApi.Models; -using OpenShock.Common.Constants; -using OpenShock.Common.DataAnnotations; -using OpenShock.Common.Models; -using OpenShock.Common.Utils; -using Asp.Versioning; -using OpenShock.Common.Extensions; -using OpenShock.Common.Authentication; - -namespace OpenShock.Common.Swagger; - -public static class SwaggerGenExtensions -{ - public static IServiceCollection AddSwaggerExt(this WebApplicationBuilder builder) where TProgram : class - { - var assembly = typeof(TProgram).Assembly; - - string assemblyName = assembly - .GetName() - .Name ?? throw new NullReferenceException("Assembly name"); - - var versions = assembly.GetAllControllerEndpointAttributes() - .SelectMany(type => type.Versions) - .Select(v => v.ToString()) - .ToHashSet() - .OrderBy(v => v) - .ToArray(); - - if (versions.Any(v => !int.TryParse(v, out _))) - { - throw new InvalidDataException($"Found invalid API versions: [{string.Join(", ", versions.Where(v => !int.TryParse(v, out _)))}]"); - } - - return builder.Services - .AddSwaggerGen(options => - { - options.CustomOperationIds(e => - $"{e.ActionDescriptor.RouteValues["controller"]}_{e.ActionDescriptor.AttributeRouteInfo?.Name ?? e.ActionDescriptor.RouteValues["action"]}"); - options.SchemaFilter(); - options.ParameterFilter(); - options.OperationFilter(); - options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, assemblyName + ".xml"), true); - options.AddSecurityDefinition(OpenShockAuthSchemes.UserSessionCookie, new OpenApiSecurityScheme - { - Name = AuthConstants.UserSessionCookieName, - Description = "Enter user session cookie", - In = ParameterLocation.Cookie, - Type = SecuritySchemeType.ApiKey, - Scheme = OpenShockAuthSchemes.UserSessionCookie, - Reference = new OpenApiReference - { - Id = OpenShockAuthSchemes.UserSessionCookie, - Type = ReferenceType.SecurityScheme, - } - }); - options.AddSecurityDefinition(OpenShockAuthSchemes.ApiToken, new OpenApiSecurityScheme - { - Name = AuthConstants.ApiTokenHeaderName, - Description = "Enter API Token", - In = ParameterLocation.Header, - Type = SecuritySchemeType.ApiKey, - Scheme = OpenShockAuthSchemes.ApiToken, - Reference = new OpenApiReference - { - Id = OpenShockAuthSchemes.ApiToken, - Type = ReferenceType.SecurityScheme, - } - }); - options.AddSecurityDefinition(OpenShockAuthSchemes.HubToken, new OpenApiSecurityScheme - { - Name = AuthConstants.HubTokenHeaderName, - Description = "Enter hub token", - In = ParameterLocation.Header, - Type = SecuritySchemeType.ApiKey, - Scheme = OpenShockAuthSchemes.HubToken, - Reference = new OpenApiReference - { - Id = OpenShockAuthSchemes.HubToken, - Type = ReferenceType.SecurityScheme, - } - }); - options.AddServer(new OpenApiServer { Url = "https://api.openshock.app" }); - options.AddServer(new OpenApiServer { Url = "https://api.openshock.dev" }); - if (builder.Environment.IsDevelopment()) - { - options.AddServer(new OpenApiServer { Url = "https://localhost" }); - } - - foreach (var version in versions) - { - options.SwaggerDoc("v" + version, new OpenApiInfo { Title = "OpenShock", Version = version }); - } - options.MapType(() => OpenApiSchemas.SemVerSchema); - options.MapType(() => OpenApiSchemas.PauseReasonEnumSchema); - - // Avoid nullable strings everywhere - options.SupportNonNullableReferenceTypes(); - }) - .ConfigureOptions(); - } -} diff --git a/Common/Utils/ConfigureSwaggerOptions.cs b/Common/Utils/ConfigureSwaggerOptions.cs deleted file mode 100644 index 2dbc2694..00000000 --- a/Common/Utils/ConfigureSwaggerOptions.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Asp.Versioning.ApiExplorer; -using Microsoft.Extensions.Options; -using Microsoft.OpenApi.Models; -using Swashbuckle.AspNetCore.SwaggerGen; - -namespace OpenShock.Common.Utils; - -public sealed class ConfigureSwaggerOptions : IConfigureNamedOptions -{ - private readonly IApiVersionDescriptionProvider _provider; - - public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) - { - _provider = provider; - } - - public void Configure(SwaggerGenOptions options) - { - // add swagger document for every API version discovered - foreach (var description in _provider.ApiVersionDescriptions) - options.SwaggerDoc( - description.GroupName, - CreateVersionInfo(description)); - } - - public void Configure(string? name, SwaggerGenOptions options) => Configure(options); - - private static OpenApiInfo CreateVersionInfo( - ApiVersionDescription description) - { - var info = new OpenApiInfo - { - Title = "OpenShock.API", - Version = description.ApiVersion.ToString() - }; - if (description.IsDeprecated) info.Description += " This API version has been deprecated."; - return info; - } -} \ No newline at end of file diff --git a/Common/Utils/TrustedProxiesFetcher.cs b/Common/Utils/TrustedProxiesFetcher.cs index b5acb452..1a3142e9 100644 --- a/Common/Utils/TrustedProxiesFetcher.cs +++ b/Common/Utils/TrustedProxiesFetcher.cs @@ -1,4 +1,4 @@ -using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork; +using System.Net; namespace OpenShock.Common.Utils; @@ -23,7 +23,7 @@ public static class TrustedProxiesFetcher "fe80::/10", ]; - private static readonly IPNetwork[] PrivateNetworksParsed = [.. PrivateNetworks.Select(x => IPNetwork.Parse(x))]; + private static readonly IPNetwork[] PrivateNetworksParsed = [.. PrivateNetworks.Select(IPNetwork.Parse)]; private static readonly char[] NewLineSeperators = ['\r', '\n', '\t']; diff --git a/Cron/Program.cs b/Cron/Program.cs index 43b5cbb2..011ffaab 100644 --- a/Cron/Program.cs +++ b/Cron/Program.cs @@ -2,9 +2,9 @@ using Hangfire.PostgreSql; using OpenShock.Common; using OpenShock.Common.Extensions; +using OpenShock.Common.OpenAPI; using OpenShock.Cron; using OpenShock.Cron.Utils; -using OpenShock.Common.Swagger; var builder = OpenShockApplication.CreateDefaultBuilder(args); @@ -21,7 +21,7 @@ c.UseNpgsqlConnection(databaseOptions.Conn))); builder.Services.AddHangfireServer(); -builder.AddSwaggerExt(); +builder.AddOpenApiExt(); var app = builder.Build(); diff --git a/Directory.Build.props b/Directory.Build.props index 0aed8b31..a197bfa9 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -22,7 +22,7 @@ - net9.0 + net10.0 @@ -35,6 +35,7 @@ $(Version) a2109c1e-fb11-44d7-8127-346ef60cb9a5 true + $(InterceptorsNamespaces);Microsoft.AspNetCore.OpenApi.Generated diff --git a/Directory.Packages.props b/Directory.Packages.props index a05a5fea..66947fa9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,7 +5,7 @@ - + @@ -14,16 +14,17 @@ - - - - - - - - + + + + + + + + + - + @@ -33,14 +34,13 @@ - + - - - + + \ No newline at end of file diff --git a/LiveControlGateway/Controllers/HubControllerBase.cs b/LiveControlGateway/Controllers/HubControllerBase.cs index f8a91f0d..6f21f088 100644 --- a/LiveControlGateway/Controllers/HubControllerBase.cs +++ b/LiveControlGateway/Controllers/HubControllerBase.cs @@ -1,4 +1,5 @@ -using FlatSharp; +using System.Diagnostics.CodeAnalysis; +using FlatSharp; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using OneOf; @@ -37,17 +38,16 @@ public abstract class HubControllerBase : FlatbuffersWebsocketBaseCon /// Service provider /// protected readonly IServiceProvider ServiceProvider; - - private HubLifetime? _hubLifetime; - + /// /// Hub lifetime /// /// + [field: AllowNull, MaybeNull] protected HubLifetime HubLifetime { - get => _hubLifetime ?? throw new InvalidOperationException("Hub lifetime is null but was tried to access"); - private set => _hubLifetime = value; + get => field ?? throw new InvalidOperationException("Hub lifetime is null but was tried to access"); + private set; } private readonly LcgOptions _options; diff --git a/LiveControlGateway/Program.cs b/LiveControlGateway/Program.cs index 8de00eef..b68d399f 100644 --- a/LiveControlGateway/Program.cs +++ b/LiveControlGateway/Program.cs @@ -2,10 +2,10 @@ using Microsoft.Extensions.Options; using OpenShock.Common; using OpenShock.Common.Extensions; +using OpenShock.Common.OpenAPI; using OpenShock.Common.Services; using OpenShock.Common.Services.Device; using OpenShock.Common.Services.Ota; -using OpenShock.Common.Swagger; using OpenShock.LiveControlGateway; using OpenShock.LiveControlGateway.LifetimeManager; using OpenShock.LiveControlGateway.Options; @@ -34,7 +34,7 @@ builder.Services.AddScoped(); builder.Services.AddKeyedSingleton("OpenShock.Gateway.Meter", new Meter("OpenShock.Gateway", "1.0.0", [new KeyValuePair("gateway_fqdn", lcgOptions.Fqdn)])); -builder.AddSwaggerExt(); +builder.AddOpenApiExt(); //services.AddHealthChecks().AddCheck("database"); diff --git a/docker/API.Dockerfile b/docker/API.Dockerfile index 92247044..2e67a470 100644 --- a/docker/API.Dockerfile +++ b/docker/API.Dockerfile @@ -10,7 +10,7 @@ COPY --link API/. API/ RUN dotnet publish --no-restore -c Release API/API.csproj -o /app # final is the final runtime stage for running the app -FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS final-api +FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final-api WORKDIR /app COPY docker/entrypoint.sh /entrypoint.sh diff --git a/docker/Base.Dockerfile b/docker/Base.Dockerfile index 766d5bfe..0768d286 100644 --- a/docker/Base.Dockerfile +++ b/docker/Base.Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine AS build-common +FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build-common WORKDIR /src COPY --link Common/*.csproj Common/ diff --git a/docker/Cron.Dockerfile b/docker/Cron.Dockerfile index 10890695..1836c4b9 100644 --- a/docker/Cron.Dockerfile +++ b/docker/Cron.Dockerfile @@ -10,7 +10,7 @@ COPY --link Cron/. Cron/ RUN dotnet publish --no-restore -c Release Cron/Cron.csproj -o /app # final is the final runtime stage for running the app -FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS final-cron +FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final-cron WORKDIR /app COPY docker/entrypoint.sh /entrypoint.sh diff --git a/docker/LiveControlGateway.Dockerfile b/docker/LiveControlGateway.Dockerfile index 9f302cba..5d4a8107 100644 --- a/docker/LiveControlGateway.Dockerfile +++ b/docker/LiveControlGateway.Dockerfile @@ -10,7 +10,7 @@ COPY --link LiveControlGateway/. LiveControlGateway/ RUN dotnet publish --no-restore -c Release LiveControlGateway/LiveControlGateway.csproj -o /app # final is the final runtime stage for running the app -FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine AS final-gateway +FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final-gateway WORKDIR /app COPY docker/entrypoint.sh /entrypoint.sh diff --git a/global.json b/global.json index 485649e1..2e206e82 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,10 @@ { "sdk": { - "version": "9.0.0", + "version": "10.0.0", "rollForward": "latestMinor", "allowPrerelease": false + }, + "test": { + "runner": "Microsoft.Testing.Platform" } }