diff --git a/API/API.csproj b/API/API.csproj index 5e91d68a..57b7be35 100644 --- a/API/API.csproj +++ b/API/API.csproj @@ -36,7 +36,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/API/Program.cs b/API/Program.cs index bfcfa02b..1ed81d91 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -1,49 +1,123 @@ +using Microsoft.AspNetCore.Http.Connections; +using Microsoft.EntityFrameworkCore; using OpenShock.API; +using OpenShock.API.Realtime; +using OpenShock.API.Services; +using OpenShock.API.Services.Account; +using OpenShock.API.Services.Email.Mailjet; +using OpenShock.API.Services.Email.Smtp; +using OpenShock.Common; +using OpenShock.Common.Extensions; +using OpenShock.Common.Hubs; +using OpenShock.Common.JsonSerialization; +using OpenShock.Common.OpenShockDb; +using OpenShock.Common.Services.Device; +using OpenShock.Common.Services.LCGNodeProvisioner; +using OpenShock.Common.Services.Ota; +using OpenShock.Common.Services.Turnstile; +using OpenShock.Common.Utils; +using Scalar.AspNetCore; using Serilog; -HostBuilder builder = new(); -builder.UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureHostConfiguration(config => - { - config.AddEnvironmentVariables(prefix: "DOTNET_"); - if (args is { Length: > 0 }) config.AddCommandLine(args); - }) - .ConfigureAppConfiguration((context, config) => - { - config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false) - .AddJsonFile("appsettings.Custom.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{context.HostingEnvironment.EnvironmentName}.json", optional: true, - reloadOnChange: false); - - config.AddUserSecrets(typeof(Program).Assembly); - config.AddEnvironmentVariables(); - if (args is { Length: > 0 }) config.AddCommandLine(args); - }) - .UseDefaultServiceProvider((context, options) => +var builder = OpenShockApplication.CreateDefaultBuilder(args, options => +{ + options.ListenAnyIP(80); +#if DEBUG + options.ListenAnyIP(443, options => options.UseHttps()); +#endif +}); + +var config = builder.GetAndRegisterOpenShockConfig(); +var commonServices = builder.Services.AddOpenShockServices(config); + +builder.Services.AddSignalR() + .AddOpenShockStackExchangeRedis(options => { options.Configuration = commonServices.RedisConfig; }) + .AddJsonProtocol(options => { - var isDevelopment = context.HostingEnvironment.IsDevelopment(); - options.ValidateScopes = isDevelopment; - options.ValidateOnBuild = isDevelopment; - }) - .UseSerilog((context, _, config) => { config.ReadFrom.Configuration(context.Configuration); }) - .ConfigureWebHostDefaults(webBuilder => + options.PayloadSerializerOptions.PropertyNameCaseInsensitive = true; + options.PayloadSerializerOptions.Converters.Add(new SemVersionJsonConverter()); + }); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddSwaggerExt("OpenShock.API"); + +builder.Services.AddSingleton(); + +builder.Services.AddSingleton(x => +{ + return new CloudflareTurnstileOptions { - webBuilder.UseKestrel(); - webBuilder.ConfigureKestrel(serverOptions => + SecretKey = config.Turnstile.SecretKey ?? string.Empty, + SiteKey = config.Turnstile.SiteKey ?? string.Empty + }; +}); +builder.Services.AddHttpClient(); + +// ----------------- MAIL SETUP ----------------- +var emailConfig = config.Mail; +switch (emailConfig.Type) +{ + case ApiConfig.MailConfig.MailType.Mailjet: + if (emailConfig.Mailjet == null) + throw new Exception("Mailjet config is null but mailjet is selected as mail type"); + builder.Services.AddMailjetEmailService(emailConfig.Mailjet, emailConfig.Sender); + break; + case ApiConfig.MailConfig.MailType.Smtp: + if (emailConfig.Smtp == null) + throw new Exception("SMTP config is null but SMTP is selected as mail type"); + builder.Services.AddSmtpEmailService(emailConfig.Smtp, emailConfig.Sender, new SmtpServiceTemplates { - serverOptions.ListenAnyIP(80); -#if DEBUG - serverOptions.ListenAnyIP(443, options => { options.UseHttps(); }); -#endif - serverOptions.Limits.RequestHeadersTimeout = TimeSpan.FromMilliseconds(3000); + PasswordReset = SmtpTemplate.ParseFromFileThrow("SmtpTemplates/PasswordReset.liquid").Result, + EmailVerification = SmtpTemplate.ParseFromFileThrow("SmtpTemplates/EmailVerification.liquid").Result }); - webBuilder.UseStartup(); - }); -try + break; + default: + throw new Exception("Unknown mail type"); +} + +builder.Services.ConfigureOptions(); +//services.AddHealthChecks().AddCheck("database"); + +builder.Services.AddHostedService(); + +var app = builder.Build(); + +app.UseCommonOpenShockMiddleware(); + +if (!config.Db.SkipMigration) { - await builder.Build().RunAsync(); + Log.Information("Running database migrations..."); + using var scope = app.Services.CreateScope(); + var openShockContext = scope.ServiceProvider.GetRequiredService(); + var pendingMigrations = openShockContext.Database.GetPendingMigrations().ToList(); + + if (pendingMigrations.Count > 0) + { + Log.Information("Found pending migrations, applying [{@Migrations}]", pendingMigrations); + openShockContext.Database.Migrate(); + Log.Information("Applied database migrations... proceeding with startup"); + } + else + { + Log.Information("No pending migrations found, proceeding with startup"); + } } -catch (Exception e) +else { - Console.WriteLine(e); -} \ No newline at end of file + Log.Warning("Skipping possible database migrations..."); +} + +app.UseSwaggerExt(); + +app.MapControllers(); + +app.MapHub("/1/hubs/user", options => options.Transports = HttpTransportType.WebSockets); +app.MapHub("/1/hubs/share/link/{id:guid}", options => options.Transports = HttpTransportType.WebSockets); + +app.MapScalarApiReference(options => options.OpenApiRoutePattern = "/swagger/{documentName}/swagger.json"); + +app.Run(); diff --git a/API/Properties/launchSettings.json b/API/Properties/launchSettings.json index c027337d..abfc1566 100644 --- a/API/Properties/launchSettings.json +++ b/API/Properties/launchSettings.json @@ -1,18 +1,10 @@ { "profiles": { - "ShockLink": { + "API": { "commandName": "Project", "dotnetRunMessages": true, "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "DB": "Host=docker-node;Port=1337;Database=root;Username=root;Password=root;Search Path=ShockLink", - "REDIS_HOST":"docker-node", - "REDIS_PASSWORD": "", - "CF_ACC_ID": "", - "CF_IMG_KEY": "", - "CF_IMG_URL": "", - "MAILJET_KEY": "", - "MAILJET_SECRET": "" + "ASPNETCORE_ENVIRONMENT": "Development" } } } diff --git a/API/Startup.cs b/API/Startup.cs deleted file mode 100644 index ce6df6d8..00000000 --- a/API/Startup.cs +++ /dev/null @@ -1,216 +0,0 @@ -using System.Text; -using Asp.Versioning.ApiExplorer; -using Microsoft.AspNetCore.Http.Connections; -using Microsoft.EntityFrameworkCore; -using Microsoft.OpenApi.Models; -using OpenShock.API.Realtime; -using OpenShock.API.Services; -using OpenShock.API.Services.Account; -using OpenShock.API.Services.Email.Mailjet; -using OpenShock.API.Services.Email.Smtp; -using OpenShock.Common; -using OpenShock.Common.Constants; -using OpenShock.Common.DataAnnotations; -using OpenShock.Common.Hubs; -using OpenShock.Common.JsonSerialization; -using OpenShock.Common.Models; -using OpenShock.Common.OpenShockDb; -using OpenShock.Common.Redis; -using OpenShock.Common.Services.Device; -using OpenShock.Common.Services.LCGNodeProvisioner; -using OpenShock.Common.Services.Ota; -using OpenShock.Common.Services.Session; -using OpenShock.Common.Services.Turnstile; -using OpenShock.Common.Utils; -using Redis.OM; -using Redis.OM.Contracts; -using Scalar.AspNetCore; -using Semver; -using Serilog; - -namespace OpenShock.API; - -public sealed class Startup -{ - - private readonly ApiConfig _apiConfig; - - public Startup(IConfiguration configuration) - { - _apiConfig = configuration.GetChildren() - .FirstOrDefault(x => x.Key.Equals("openshock", StringComparison.InvariantCultureIgnoreCase))? - .Get() ?? - throw new Exception("Couldn't bind config, check config file"); - - var startupLogger = new LoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger(); - - MiniValidation.MiniValidator.TryValidate(_apiConfig, true, true, out var errors); - if (errors.Count > 0) - { - var sb = new StringBuilder(); - - foreach (var error in errors) - { - sb.AppendLine($"Error on field [{error.Key}] reason: {string.Join(", ", error.Value)}"); - } - - startupLogger.Error( - "Error validating config, please fix your configuration / environment variables\nFound the following errors:\n{Errors}", - sb.ToString()); - Environment.Exit(-10); - } - } - - public void ConfigureServices(IServiceCollection services) - { - services.AddSingleton(_apiConfig); - - var commonServices = services.AddOpenShockServices(_apiConfig); - - services.AddSingleton(); - - services.AddSingleton(x => - { - var config = x.GetRequiredService(); - return new CloudflareTurnstileOptions - { - SecretKey = config.Turnstile.SecretKey ?? string.Empty, - SiteKey = config.Turnstile.SiteKey ?? string.Empty - }; - }); - services.AddHttpClient(); - - // ----------------- MAIL SETUP ----------------- - var emailConfig = _apiConfig.Mail; - switch (emailConfig.Type) - { - case ApiConfig.MailConfig.MailType.Mailjet: - if (emailConfig.Mailjet == null) - throw new Exception("Mailjet config is null but mailjet is selected as mail type"); - services.AddMailjetEmailService(emailConfig.Mailjet, emailConfig.Sender); - break; - case ApiConfig.MailConfig.MailType.Smtp: - if (emailConfig.Smtp == null) - throw new Exception("SMTP config is null but SMTP is selected as mail type"); - services.AddSmtpEmailService(emailConfig.Smtp, emailConfig.Sender, new SmtpServiceTemplates - { - PasswordReset = SmtpTemplate.ParseFromFileThrow("SmtpTemplates/PasswordReset.liquid").Result, - EmailVerification = SmtpTemplate.ParseFromFileThrow("SmtpTemplates/EmailVerification.liquid").Result - }); - break; - default: - throw new Exception("Unknown mail type"); - } - - services.AddSignalR() - .AddOpenShockStackExchangeRedis(options => { options.Configuration = commonServices.RedisConfig; }) - .AddJsonProtocol(options => - { - options.PayloadSerializerOptions.PropertyNameCaseInsensitive = true; - options.PayloadSerializerOptions.Converters.Add(new SemVersionJsonConverter()); - }); - - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - - 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, "OpenShock.API.xml"), true); - options.AddSecurityDefinition(AuthConstants.AuthTokenHeaderName, new OpenApiSecurityScheme - { - Name = AuthConstants.AuthTokenHeaderName, - Type = SecuritySchemeType.ApiKey, - Scheme = "ApiKeyAuth", - In = ParameterLocation.Header, - Description = "API Token Authorization header." - }); - options.AddSecurityRequirement(new OpenApiSecurityRequirement - { - { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = AuthConstants.AuthTokenHeaderName - } - }, - Array.Empty() - } - }); - options.AddServer(new OpenApiServer { Url = "https://api.openshock.app" }); - options.AddServer(new OpenApiServer { Url = "https://staging-api.openshock.app" }); -#if DEBUG - options.AddServer(new OpenApiServer { Url = "https://localhost" }); -#endif - options.SwaggerDoc("v1", new OpenApiInfo { Title = "OpenShock", Version = "1" }); - options.SwaggerDoc("v2", new OpenApiInfo { Title = "OpenShock", Version = "2" }); - options.MapType(() => OpenApiSchemas.SemVerSchema); - options.MapType(() => OpenApiSchemas.PauseReasonEnumSchema); - - // Avoid nullable strings everywhere - options.SupportNonNullableReferenceTypes(); - } - ); - - services.ConfigureOptions(); - //services.AddHealthChecks().AddCheck("database"); - - services.AddHostedService(); - } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory, - ILogger logger) - { - ApplicationLogging.LoggerFactory = loggerFactory; - - app.UseCommonOpenShockMiddleware(); - - if (!_apiConfig.Db.SkipMigration) - { - logger.LogInformation("Running database migrations..."); - using var scope = app.ApplicationServices.CreateScope(); - var openShockContext = scope.ServiceProvider.GetRequiredService(); - var pendingMigrations = openShockContext.Database.GetPendingMigrations().ToList(); - - if (pendingMigrations.Count > 0) - { - logger.LogInformation("Found pending migrations, applying [{@Migrations}]", pendingMigrations); - openShockContext.Database.Migrate(); - logger.LogInformation("Applied database migrations... proceeding with startup"); - } - else logger.LogInformation("No pending migrations found, proceeding with startup"); - } - else logger.LogWarning("Skipping possible database migrations..."); - - app.UseSwagger(); - var provider = app.ApplicationServices.GetRequiredService(); - app.UseSwaggerUI(c => - { - foreach (var description in provider.ApiVersionDescriptions) - c.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", - description.GroupName.ToUpperInvariant()); - }); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - endpoints.MapHub("/1/hubs/user", - options => { options.Transports = HttpTransportType.WebSockets; }); - endpoints.MapHub("/1/hubs/share/link/{id}", - options => { options.Transports = HttpTransportType.WebSockets; }); - - endpoints.MapScalarApiReference(options => - { - options.OpenApiRoutePattern = "/swagger/{documentName}/swagger.json"; - }); - }); - } -} \ No newline at end of file diff --git a/Common/ApplicationLogging.cs b/Common/ApplicationLogging.cs deleted file mode 100644 index 2fd5be32..00000000 --- a/Common/ApplicationLogging.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.Extensions.Logging; - -namespace OpenShock.Common; - -public static class ApplicationLogging -{ - public static ILoggerFactory LoggerFactory { get; set; } = null!; - public static ILogger CreateLogger() => LoggerFactory.CreateLogger(); - public static ILogger CreateLogger(Type type) => LoggerFactory.CreateLogger(type); - public static ILogger CreateLogger(string categoryName) => LoggerFactory.CreateLogger(categoryName); -} \ No newline at end of file diff --git a/Common/Common.csproj b/Common/Common.csproj index 6cefdf4c..1c1a5822 100644 --- a/Common/Common.csproj +++ b/Common/Common.csproj @@ -14,7 +14,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -29,7 +29,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + + @@ -40,12 +41,12 @@ - + - + - - + + diff --git a/Common/ExceptionHandle/ExceptionHandler.cs b/Common/ExceptionHandle/ExceptionHandler.cs index bb6679d9..6c7b57c6 100644 --- a/Common/ExceptionHandle/ExceptionHandler.cs +++ b/Common/ExceptionHandle/ExceptionHandler.cs @@ -1,21 +1,18 @@ using System.Net; using Microsoft.AspNetCore.Diagnostics; -using Microsoft.AspNetCore.Http.Json; -using Microsoft.Extensions.Options; using OpenShock.Common.Errors; namespace OpenShock.Common.ExceptionHandle; public sealed class OpenShockExceptionHandler : IExceptionHandler { - private static readonly ILogger Logger = ApplicationLogging.CreateLogger(typeof(OpenShockExceptionHandler)); - private static readonly ILogger LoggerRequestInfo = ApplicationLogging.CreateLogger("RequestInfo"); - private readonly IProblemDetailsService _problemDetailsService; - - public OpenShockExceptionHandler(IProblemDetailsService problemDetailsService) + private readonly ILogger _logger; + + public OpenShockExceptionHandler(IProblemDetailsService problemDetailsService, ILoggerFactory loggerFactory) { _problemDetailsService = problemDetailsService; + _logger = loggerFactory.CreateLogger("RequestInfo"); } public async ValueTask TryHandleAsync(HttpContext context, Exception exception, CancellationToken cancellationToken) @@ -34,7 +31,7 @@ public async ValueTask TryHandleAsync(HttpContext context, Exception excep }); } - private static async Task PrintRequestInfo(HttpContext context) + private async Task PrintRequestInfo(HttpContext context) { // Rewind our body reader, so we can read it again. context.Request.Body.Seek(0, SeekOrigin.Begin); @@ -57,6 +54,6 @@ private static async Task PrintRequestInfo(HttpContext context) }; // Finally log this object on Information level. - LoggerRequestInfo.LogInformation("{@RequestInfo}", requestInfo); + _logger.LogInformation("{@RequestInfo}", requestInfo); } } \ No newline at end of file diff --git a/Common/Extensions/ConfigurationExtensions.cs b/Common/Extensions/ConfigurationExtensions.cs new file mode 100644 index 00000000..90509578 --- /dev/null +++ b/Common/Extensions/ConfigurationExtensions.cs @@ -0,0 +1,45 @@ +using OpenShock.Common.Config; +using Serilog; +using System.Text; +using System.Text.Json; + +namespace OpenShock.Common.Extensions; + +public static class ConfigurationExtensions +{ + public static T GetAndRegisterOpenShockConfig(this WebApplicationBuilder builder) where T : BaseConfig + { +#if DEBUG + Console.WriteLine(builder.Configuration.GetDebugView()); +#endif + + var config = builder.Configuration + .GetChildren() + .First(x => x.Key.Equals("openshock", StringComparison.InvariantCultureIgnoreCase)) + .Get() ?? throw new Exception("Couldn't bind config, check config file"); + + MiniValidation.MiniValidator.TryValidate(config, true, true, out var errors); + if (errors.Count > 0) + { + var sb = new StringBuilder(); + + sb.AppendLine("Error validating config, please fix your configuration / environment variables"); + sb.AppendLine("Found the following errors:"); + foreach (var error in errors) + { + sb.AppendLine($"Error on field [{error.Key}] reason: {string.Join(", ", error.Value)}"); + } + + Log.Error(sb.ToString()); + Environment.Exit(-10); + } + +#if DEBUG + Console.WriteLine(JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true })); +#endif + + builder.Services.AddSingleton(config); + + return config; + } +} diff --git a/Common/Extensions/SwaggerGenExtensions.cs b/Common/Extensions/SwaggerGenExtensions.cs new file mode 100644 index 00000000..5f0c3d21 --- /dev/null +++ b/Common/Extensions/SwaggerGenExtensions.cs @@ -0,0 +1,74 @@ +using Microsoft.OpenApi.Models; +using OpenShock.Common.Constants; +using OpenShock.Common.DataAnnotations; +using OpenShock.Common.Models; +using Semver; +using Asp.Versioning.ApiExplorer; + +namespace OpenShock.Common.Extensions; + +public static class SwaggerGenExtensions +{ + public static IServiceCollection AddSwaggerExt(this IServiceCollection services, string assemblyName) + { + return 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(AuthConstants.AuthTokenHeaderName, new OpenApiSecurityScheme + { + Name = AuthConstants.AuthTokenHeaderName, + Type = SecuritySchemeType.ApiKey, + Scheme = "ApiKeyAuth", + In = ParameterLocation.Header, + Description = "API Token Authorization header." + }); + options.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = AuthConstants.AuthTokenHeaderName + } + }, + Array.Empty() + } + }); + options.AddServer(new OpenApiServer { Url = "https://api.openshock.app" }); + options.AddServer(new OpenApiServer { Url = "https://staging-api.openshock.app" }); +#if DEBUG + options.AddServer(new OpenApiServer { Url = "https://localhost" }); +#endif + options.SwaggerDoc("v1", new OpenApiInfo { Title = "OpenShock", Version = "1" }); + options.SwaggerDoc("v2", new OpenApiInfo { Title = "OpenShock", Version = "2" }); + options.MapType(() => OpenApiSchemas.SemVerSchema); + options.MapType(() => OpenApiSchemas.PauseReasonEnumSchema); + + // Avoid nullable strings everywhere + options.SupportNonNullableReferenceTypes(); + }); + } + + public static IApplicationBuilder UseSwaggerExt(this WebApplication app) + { + var provider = app.Services.GetRequiredService(); + var groupNames = provider.ApiVersionDescriptions.Select(d => d.GroupName).ToList(); + + return app + .UseSwagger() + .UseSwaggerUI(c => + { + foreach (var groupName in groupNames) + { + c.SwaggerEndpoint($"/swagger/{groupName}/swagger.json", groupName.ToUpperInvariant()); + } + }); + } +} diff --git a/Common/OpenShockApplication.cs b/Common/OpenShockApplication.cs new file mode 100644 index 00000000..523fc940 --- /dev/null +++ b/Common/OpenShockApplication.cs @@ -0,0 +1,42 @@ +using System.Reflection; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Serilog; + +namespace OpenShock.Common; + +public static class OpenShockApplication +{ + public static WebApplicationBuilder CreateDefaultBuilder(string[] args, Action configurePorts) where TProgram : class + { + var builder = WebApplication.CreateSlimBuilder(args); + + builder.Configuration.Sources.Clear(); + builder.Configuration + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) + .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: false) + .AddJsonFile("appsettings.Custom.json", optional: true, reloadOnChange: false) + .AddEnvironmentVariables() + .AddUserSecrets(true) + .AddCommandLine(args); + + var isDevelopment = builder.Environment.IsDevelopment(); + builder.Host.UseDefaultServiceProvider((_, options) => + { + options.ValidateScopes = isDevelopment; + options.ValidateOnBuild = isDevelopment; + }); + + // Since we use slim builders, this allows for HTTPS during local development + if (isDevelopment) builder.WebHost.UseKestrelHttpsConfiguration(); + + builder.WebHost.ConfigureKestrel(serverOptions => + { + configurePorts(serverOptions); + serverOptions.Limits.RequestHeadersTimeout = TimeSpan.FromMilliseconds(3000); + }); + + builder.Host.UseSerilog((context, _, config) => config.ReadFrom.Configuration(context.Configuration)); + + return builder; + } +} diff --git a/Common/Utils/LucTask.cs b/Common/Utils/LucTask.cs index 44cec6f2..02a93019 100644 --- a/Common/Utils/LucTask.cs +++ b/Common/Utils/LucTask.cs @@ -1,12 +1,10 @@ -using System.Runtime.CompilerServices; -using Microsoft.Extensions.Logging; +using Serilog; +using System.Runtime.CompilerServices; namespace OpenShock.Common.Utils; public static class LucTask { - private static readonly ILogger Logger = ApplicationLogging.CreateLogger(typeof(LucTask)); - public static Task Run(Func function, [CallerFilePath] string file = "", [CallerMemberName] string member = "", [CallerLineNumber] int line = -1) => Task.Run(function).ContinueWith( t => @@ -14,9 +12,9 @@ public static Task Run(Func function, [CallerFilePath] string file = "", if (!t.IsFaulted) return; var index = file.LastIndexOf('\\'); if (index == -1) index = file.LastIndexOf('/'); - Logger.LogError(t.Exception, + Log.Error(t.Exception, "Error during task execution. {File}::{Member}:{Line} - Stack: {Stack}", - file.Substring(index + 1, file.Length - index - 1), member, line, t.Exception?.StackTrace); + file[(index + 1)..], member, line, t.Exception?.StackTrace); }, TaskContinuationOptions.OnlyOnFaulted); @@ -27,9 +25,9 @@ public static Task Run(Task? function, [CallerFilePath] string file = "", if (!t.IsFaulted) return; var index = file.LastIndexOf('\\'); if (index == -1) index = file.LastIndexOf('/'); - Logger.LogError(t.Exception, + Log.Error(t.Exception, "Error during task execution. {File}::{Member}:{Line} - Stack: {Stack}", - file.Substring(index + 1, file.Length - index - 1), member, line, t.Exception?.StackTrace); + file[(index + 1)..], member, line, t.Exception?.StackTrace); }, TaskContinuationOptions.OnlyOnFaulted); } \ No newline at end of file diff --git a/Cron/Program.cs b/Cron/Program.cs index 1dfe7397..a353f2e9 100644 --- a/Cron/Program.cs +++ b/Cron/Program.cs @@ -1,60 +1,43 @@ using Hangfire; +using Hangfire.PostgreSql; +using OpenShock.Common; +using OpenShock.Common.Extensions; using OpenShock.Cron; -using OpenShock.Cron.Jobs; using OpenShock.Cron.Utils; -using Serilog; - -HostBuilder builder = new(); -builder.UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureHostConfiguration(config => - { - config.AddEnvironmentVariables(prefix: "DOTNET_"); - if (args is { Length: > 0 }) config.AddCommandLine(args); - }) - .ConfigureAppConfiguration((context, config) => - { - config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false) - .AddJsonFile($"appsettings.{context.HostingEnvironment.EnvironmentName}.json", optional: true, - reloadOnChange: false); - - config.AddUserSecrets(typeof(Program).Assembly); - config.AddEnvironmentVariables(); - if (args is { Length: > 0 }) config.AddCommandLine(args); - }) - .UseDefaultServiceProvider((context, options) => - { - var isDevelopment = context.HostingEnvironment.IsDevelopment(); - options.ValidateScopes = isDevelopment; - options.ValidateOnBuild = isDevelopment; - }) - .UseSerilog((context, _, config) => { config.ReadFrom.Configuration(context.Configuration); }) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseKestrel(); - webBuilder.ConfigureKestrel(serverOptions => - { - serverOptions.ListenAnyIP(780); + +var builder = OpenShockApplication.CreateDefaultBuilder(args, options => +{ + options.ListenAnyIP(780); #if DEBUG - serverOptions.ListenAnyIP(7443, options => { options.UseHttps("devcert.pfx"); }); + options.ListenAnyIP(7443, options => options.UseHttps("devcert.pfx")); #endif - serverOptions.Limits.RequestHeadersTimeout = TimeSpan.FromMilliseconds(3000); - }); - webBuilder.UseStartup(); - }); -try -{ - var app = builder.Build(); +}); - var jobManagerV2 = app.Services.GetRequiredService(); - foreach (var cronJob in CronJobCollector.GetAllCronJobs()) - { - jobManagerV2.AddOrUpdate(cronJob.Name, cronJob.Job, cronJob.Schedule); - } +var config = builder.GetAndRegisterOpenShockConfig(); +builder.Services.AddOpenShockServices(config); - await app.RunAsync(); +builder.Services.AddHangfire(hangfire => + hangfire.UsePostgreSqlStorage(c => + c.UseNpgsqlConnection(config.Db.Conn))); +builder.Services.AddHangfireServer(); -} -catch (Exception e) +var app = builder.Build(); + +app.UseCommonOpenShockMiddleware(); + +app.UseHangfireDashboard(options: new DashboardOptions { - Console.WriteLine(e); -} \ No newline at end of file + AsyncAuthorization = [ + new DashboardAdminAuth() + ] +}); + +app.MapControllers(); + +var jobManager = app.Services.GetRequiredService(); +foreach (var cronJob in CronJobCollector.GetAllCronJobs()) +{ + jobManager.AddOrUpdate(cronJob.Name, cronJob.Job, cronJob.Schedule); +} + +app.Run(); \ No newline at end of file diff --git a/Cron/Properties/launchSettings.json b/Cron/Properties/launchSettings.json index 19454ce9..0121b90c 100644 --- a/Cron/Properties/launchSettings.json +++ b/Cron/Properties/launchSettings.json @@ -1,5 +1,4 @@ { - "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { "Cron": { "commandName": "Project", diff --git a/Cron/Startup.cs b/Cron/Startup.cs deleted file mode 100644 index 1a852840..00000000 --- a/Cron/Startup.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Hangfire; -using Hangfire.PostgreSql; -using OpenShock.Common; -using OpenTelemetry.Trace; - -namespace OpenShock.Cron; - -public sealed class Startup -{ - private CronConf _config; - - public Startup(IConfiguration configuration) - { -#if DEBUG - var root = (IConfigurationRoot)configuration; - var debugView = root.GetDebugView(); - Console.WriteLine(debugView); -#endif - _config = configuration.GetChildren() - .First(x => x.Key.Equals("openshock", StringComparison.InvariantCultureIgnoreCase)) - .Get() ?? - throw new Exception("Couldn't bind config, check config file"); - } - - public void ConfigureServices(IServiceCollection services) - { - services.AddHangfire(hangfire => - hangfire.UsePostgreSqlStorage(c => - c.UseNpgsqlConnection(_config.Db.Conn))); - services.AddHangfireServer(); - - - services.AddOpenShockServices(_config); - } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory, - ILogger logger) - { - app.UseCommonOpenShockMiddleware(); - - app.UseHangfireDashboard(options: new DashboardOptions - { - AsyncAuthorization = [ - new DashboardAdminAuth() - ] - }); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } -} \ No newline at end of file diff --git a/LiveControlGateway/LifetimeManager/HubLifetime.cs b/LiveControlGateway/LifetimeManager/HubLifetime.cs index bb42077a..1c50ca6c 100644 --- a/LiveControlGateway/LifetimeManager/HubLifetime.cs +++ b/LiveControlGateway/LifetimeManager/HubLifetime.cs @@ -28,7 +28,6 @@ public sealed class HubLifetime : IAsyncDisposable { private readonly TimeSpan _waitBetweenTicks; private readonly ushort _commandDuration; - private static readonly ILogger Logger = ApplicationLogging.CreateLogger(); private Dictionary _shockerStates = new(); private readonly byte _tps; @@ -39,19 +38,23 @@ public sealed class HubLifetime : IAsyncDisposable private readonly IRedisConnectionProvider _redisConnectionProvider; private readonly IRedisPubService _redisPubService; + private readonly ILogger _logger; + /// /// DI Constructor /// /// /// /// + /// /// + /// /// - /// public HubLifetime([Range(1, 10)] byte tps, IHubController hubController, IDbContextFactory dbContextFactory, IRedisConnectionProvider redisConnectionProvider, IRedisPubService redisPubService, + ILogger logger, CancellationToken cancellationToken = default) { _tps = tps; @@ -60,6 +63,7 @@ public HubLifetime([Range(1, 10)] byte tps, IHubController hubController, _dbContextFactory = dbContextFactory; _redisConnectionProvider = redisConnectionProvider; _redisPubService = redisPubService; + _logger = logger; _waitBetweenTicks = TimeSpan.FromMilliseconds(Math.Floor((float)1000 / tps)); _commandDuration = (ushort)(_waitBetweenTicks.TotalMilliseconds * 2.5); @@ -90,14 +94,14 @@ private async Task UpdateLoop() } catch (Exception e) { - Logger.LogError(e, "Error in Update()"); + _logger.LogError(e, "Error in Update()"); } var elapsed = stopwatch.Elapsed; var waitTime = _waitBetweenTicks - elapsed; if (waitTime.TotalMilliseconds < 1) { - Logger.LogWarning("Update loop running behind for device [{DeviceId}]", _hubController.Id); + _logger.LogWarning("Update loop running behind for device [{DeviceId}]", _hubController.Id); continue; } @@ -147,7 +151,7 @@ public async Task UpdateDevice() /// private async Task UpdateShockers(OpenShockContext db) { - Logger.LogDebug("Updating shockers for device [{DeviceId}]", _hubController.Id); + _logger.LogDebug("Updating shockers for device [{DeviceId}]", _hubController.Id); var ownShockers = await db.Shockers.Where(x => x.Device == _hubController.Id).Select(x => new ShockerState() { Id = x.Id, @@ -196,7 +200,7 @@ public ValueTask Control(IEnumerable shocks) { if (!_shockerStates.TryGetValue(shock.Id, out var state)) continue; - Logger.LogTrace( + _logger.LogTrace( "Control exclusive: {Exclusive}, type: {Type}, duration: {Duration}, intensity: {Intensity}", shock.Exclusive, shock.Type, shock.Duration, shock.Intensity); state.ExclusiveUntil = shock.Exclusive && shock.Type != ControlType.Stop diff --git a/LiveControlGateway/LifetimeManager/HubLifetimeManager.cs b/LiveControlGateway/LifetimeManager/HubLifetimeManager.cs index 1a462456..ee54b981 100644 --- a/LiveControlGateway/LifetimeManager/HubLifetimeManager.cs +++ b/LiveControlGateway/LifetimeManager/HubLifetimeManager.cs @@ -18,29 +18,33 @@ namespace OpenShock.LiveControlGateway.LifetimeManager; /// public sealed class HubLifetimeManager { - private readonly ILogger _logger; private readonly IDbContextFactory _dbContextFactory; private readonly IRedisConnectionProvider _redisConnectionProvider; private readonly IRedisPubService _redisPubService; + private readonly ILoggerFactory _loggerFactory; + private readonly ILogger _logger; private readonly ConcurrentDictionary _managers = new(); /// /// DI constructor /// - /// /// /// /// + /// public HubLifetimeManager( - ILogger logger, IDbContextFactory dbContextFactory, IRedisConnectionProvider redisConnectionProvider, - IRedisPubService redisPubService) + IRedisPubService redisPubService, + ILoggerFactory loggerFactory + ) { - _logger = logger; _dbContextFactory = dbContextFactory; _redisConnectionProvider = redisConnectionProvider; _redisPubService = redisPubService; + _loggerFactory = loggerFactory; + + _logger = _loggerFactory.CreateLogger(); } /// @@ -65,6 +69,7 @@ public async Task AddDeviceConnection(byte tps, IHubController hubC _dbContextFactory, _redisConnectionProvider, _redisPubService, + _loggerFactory.CreateLogger(), cancellationToken); await using var db = await _dbContextFactory.CreateDbContextAsync(cancellationToken); diff --git a/LiveControlGateway/Program.cs b/LiveControlGateway/Program.cs index 1a6fdf73..6ddf3fb2 100644 --- a/LiveControlGateway/Program.cs +++ b/LiveControlGateway/Program.cs @@ -1,51 +1,53 @@ +using OpenShock.Common; +using OpenShock.Common.Extensions; +using OpenShock.Common.JsonSerialization; +using OpenShock.Common.Services.Device; +using OpenShock.Common.Services.Ota; +using OpenShock.Common.Utils; using OpenShock.LiveControlGateway; -using Serilog; - -HostBuilder builder = new(); -builder.UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureHostConfiguration(config => - { - config.AddEnvironmentVariables(prefix: "DOTNET_"); - if (args is { Length: > 0 }) config.AddCommandLine(args); - }) - .ConfigureAppConfiguration((context, config) => - { - config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false) - .AddJsonFile("appsettings.Custom.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{context.HostingEnvironment.EnvironmentName}.json", optional: true, reloadOnChange: false); - - config.AddUserSecrets(typeof(Program).Assembly); - config.AddEnvironmentVariables(); - if (args is { Length: > 0 }) config.AddCommandLine(args); - }) - .UseDefaultServiceProvider((context, options) => - { - var isDevelopment = context.HostingEnvironment.IsDevelopment(); - options.ValidateScopes = isDevelopment; - options.ValidateOnBuild = isDevelopment; - }) - .UseSerilog((context, _, config) => { config.ReadFrom.Configuration(context.Configuration); }) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseKestrel(); - webBuilder.ConfigureKestrel(serverOptions => - { +using OpenShock.LiveControlGateway.LifetimeManager; +using OpenShock.LiveControlGateway.PubSub; +var builder = OpenShockApplication.CreateDefaultBuilder(args, options => +{ #if DEBUG - serverOptions.ListenAnyIP(580); - serverOptions.ListenAnyIP(5443, options => { options.UseHttps("devcert.pfx"); }); + options.ListenAnyIP(580); + options.ListenAnyIP(5443, options => options.UseHttps("devcert.pfx")); #else - serverOptions.ListenAnyIP(80); + options.ListenAnyIP(80); #endif - serverOptions.Limits.RequestHeadersTimeout = TimeSpan.FromMilliseconds(3000); - }); - webBuilder.UseStartup(); +}); + +var config = builder.GetAndRegisterOpenShockConfig(); +var commonService = builder.Services.AddOpenShockServices(config); + +builder.Services.AddSignalR() + .AddOpenShockStackExchangeRedis(options => { options.Configuration = commonService.RedisConfig; }) + .AddJsonProtocol(options => + { + options.PayloadSerializerOptions.PropertyNameCaseInsensitive = true; + options.PayloadSerializerOptions.Converters.Add(new SemVersionJsonConverter()); }); -try -{ - await builder.Build().RunAsync(); -} -catch (Exception e) -{ - Console.WriteLine(e); -} \ No newline at end of file + +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddSwaggerExt("OpenShock.LiveControlGateway"); + +builder.Services.ConfigureOptions(); +//services.AddHealthChecks().AddCheck("database"); + +builder.Services.AddHostedService(); +builder.Services.AddHostedService(); + +builder.Services.AddSingleton(); + +var app = builder.Build(); + +app.UseCommonOpenShockMiddleware(); + +app.UseSwaggerExt(); + +app.MapControllers(); + +app.Run(); \ No newline at end of file diff --git a/LiveControlGateway/Properties/launchSettings.json b/LiveControlGateway/Properties/launchSettings.json index 2b883619..0d428b5e 100644 --- a/LiveControlGateway/Properties/launchSettings.json +++ b/LiveControlGateway/Properties/launchSettings.json @@ -4,13 +4,7 @@ "commandName": "Project", "dotnetRunMessages": true, "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "OPENSHOCK__DB": "Host=docker-node;Port=1337;Database=openshock;Username=root;Password=root", - "OPENSHOCK__FQDN": "luc-lcg.lucheart.ovh", - "OPENSHOCK__COUNTRYCODE": "DE", - "OPENSHOCK__REDIS__HOST":"docker-node", - //"OPENSHOCK__REDIS__HOST":"localhost", - "OPENSHOCK__REDIS__PASSWORD": "" + "ASPNETCORE_ENVIRONMENT": "Development" } } } diff --git a/LiveControlGateway/Startup.cs b/LiveControlGateway/Startup.cs deleted file mode 100644 index 80c69aba..00000000 --- a/LiveControlGateway/Startup.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.Text.Json; -using Asp.Versioning.ApiExplorer; -using Microsoft.OpenApi.Models; -using OpenShock.Common; -using OpenShock.Common.Constants; -using OpenShock.Common.JsonSerialization; -using OpenShock.Common.Services.Device; -using OpenShock.Common.Services.Ota; -using OpenShock.Common.Utils; -using OpenShock.LiveControlGateway.LifetimeManager; -using OpenShock.LiveControlGateway.PubSub; -using JsonSerializer = System.Text.Json.JsonSerializer; - -namespace OpenShock.LiveControlGateway; - -/// -/// Startup class for the LCG -/// -public sealed class Startup -{ - private LCGConfig _lcgConfig; - - /// - /// Setup the LCG, configure config and validate - /// - /// - /// - public Startup(IConfiguration configuration) - { -#if DEBUG - var root = (IConfigurationRoot)configuration; - var debugView = root.GetDebugView(); - Console.WriteLine(debugView); -#endif - _lcgConfig = configuration.GetChildren().First(x => x.Key.Equals("openshock", StringComparison.InvariantCultureIgnoreCase)) - .Get() ?? - throw new Exception("Couldn't bind config, check config file"); - - var validator = new ValidationContext(_lcgConfig); - Validator.ValidateObject(_lcgConfig, validator, true); - -#if DEBUG - Console.WriteLine(JsonSerializer.Serialize(_lcgConfig, - new JsonSerializerOptions { WriteIndented = true })); -#endif - } - - /// - /// Configures the services for the LCG - /// - /// - public void ConfigureServices(IServiceCollection services) - { - services.AddSingleton(_lcgConfig); - - var commonService = services.AddOpenShockServices(_lcgConfig); - - services.AddSignalR() - .AddOpenShockStackExchangeRedis(options => { options.Configuration = commonService.RedisConfig; }) - .AddJsonProtocol(options => - { - options.PayloadSerializerOptions.PropertyNameCaseInsensitive = true; - options.PayloadSerializerOptions.Converters.Add(new SemVersionJsonConverter()); - }); - - services.AddScoped(); - services.AddScoped(); - - services.AddSwaggerGen(options => - { - options.CustomOperationIds(e => - $"{e.ActionDescriptor.RouteValues["controller"]}_{e.ActionDescriptor.RouteValues["action"]}_{e.HttpMethod}"); - options.SchemaFilter(); - options.ParameterFilter(); - options.OperationFilter(); - options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "OpenShock.LiveControlGateway.xml")); - options.AddSecurityDefinition("OpenShockToken", new OpenApiSecurityScheme - { - Name = "OpenShockToken", - Type = SecuritySchemeType.ApiKey, - Scheme = "ApiKeyAuth", - In = ParameterLocation.Header, - Description = "API Token Authorization header." - }); - options.AddSecurityRequirement(new OpenApiSecurityRequirement - { - { - new OpenApiSecurityScheme - { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, - Id = AuthConstants.AuthTokenHeaderName - } - }, - Array.Empty() - } - }); - options.AddServer(new OpenApiServer { Url = "https://api.openshock.app" }); - options.AddServer(new OpenApiServer { Url = "https://staging-api.openshock.app" }); - options.AddServer(new OpenApiServer { Url = "https://localhost" }); - } - ); - - services.ConfigureOptions(); - //services.AddHealthChecks().AddCheck("database"); - - services.AddHostedService(); - services.AddHostedService(); - - services.AddSingleton(); - - } - - /// - /// Register middleware and co. - /// - /// - /// - /// - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) - { - ApplicationLogging.LoggerFactory = loggerFactory; - app.UseCommonOpenShockMiddleware(); - - app.UseSwagger(); - var provider = app.ApplicationServices.GetRequiredService(); - app.UseSwaggerUI(c => - { - foreach (var description in provider.ApiVersionDescriptions) - c.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", - description.GroupName.ToUpperInvariant()); - }); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } -} \ No newline at end of file