Cleaned up the startup sequence.

This commit is contained in:
Masu Baumgartner
2024-10-28 21:30:00 +01:00
parent f6ed12fc7a
commit e02af774a9
10 changed files with 402 additions and 166 deletions

View File

@@ -0,0 +1,17 @@
using System.Collections;
namespace Moonlight.ApiServer.Helpers;
public class DatabaseContextCollection : IEnumerable<Type>
{
private readonly List<Type> Types = new();
public IEnumerator<Type> GetEnumerator() => Types.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => Types.GetEnumerator();
public void Add<T>() where T : DatabaseContext
=> Types.Add(typeof(T));
public void Remove(Type type) => Types.Remove(type);
public void Remove<T>() where T : DatabaseContext => Remove(typeof(T));
}

View File

@@ -0,0 +1,46 @@
using Microsoft.OpenApi.Models;
using MoonCore.Services;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Interfaces.Startup;
namespace Moonlight.ApiServer.Implementations.Startup;
public class ApiDocsStartup : IAppStartup, IEndpointStartup
{
private readonly ConfigService<AppConfiguration> ConfigService;
private readonly ILogger<ApiDocsStartup> Logger;
public ApiDocsStartup(ConfigService<AppConfiguration> configService, ILogger<ApiDocsStartup> logger)
{
ConfigService = configService;
Logger = logger;
}
public Task BuildApp(IHostApplicationBuilder builder)
{
if(!ConfigService.Get().Development.EnableApiDocs)
return Task.CompletedTask;
builder.Services.AddEndpointsApiExplorer();
// Configure swagger api specification generator and set the document title for the api docs to use
builder.Services.AddSwaggerGen(options => options.SwaggerDoc("main", new OpenApiInfo()
{
Title = "Moonlight API"
}));
return Task.CompletedTask;
}
public Task ConfigureApp(IApplicationBuilder app) => Task.CompletedTask;
public Task ConfigureEndpoints(IEndpointRouteBuilder routeBuilder)
{
if(!ConfigService.Get().Development.EnableApiDocs)
return Task.CompletedTask;
routeBuilder.MapSwagger("/api/swagger/{documentName}");
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,15 @@
using Moonlight.ApiServer.Database;
using Moonlight.ApiServer.Helpers;
using Moonlight.ApiServer.Interfaces.Startup;
namespace Moonlight.ApiServer.Implementations.Startup;
public class CoreDatabaseStartup : IDatabaseStartup
{
public Task ConfigureDatabase(DatabaseContextCollection collection)
{
collection.Add<CoreDataContext>();
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,7 @@
namespace Moonlight.ApiServer.Interfaces.Startup;
public interface IAppStartup
{
public Task BuildApp(IHostApplicationBuilder builder);
public Task ConfigureApp(IApplicationBuilder app);
}

View File

@@ -0,0 +1,9 @@
using Moonlight.ApiServer.Helpers;
using Moonlight.ApiServer.Models;
namespace Moonlight.ApiServer.Interfaces.Startup;
public interface IDatabaseStartup
{
public Task ConfigureDatabase(DatabaseContextCollection collection);
}

View File

@@ -0,0 +1,6 @@
namespace Moonlight.ApiServer.Interfaces.Startup;
public interface IEndpointStartup
{
public Task ConfigureEndpoints(IEndpointRouteBuilder routeBuilder);
}

View File

@@ -13,7 +13,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MoonCore" Version="1.6.7" />
<PackageReference Include="MoonCore.Extended" Version="1.1.2" />
<PackageReference Include="MoonCore.Extended" Version="1.1.3" />
<PackageReference Include="MoonCore.PluginFramework" Version="1.0.2" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>

View File

@@ -1,55 +1,19 @@
using System.Reflection;
using System.Text.Json;
using Microsoft.OpenApi.Models;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Extensions;
using MoonCore.Extended.Helpers;
using MoonCore.Extended.OAuth2.ApiServer;
using MoonCore.Extensions;
using MoonCore.Helpers;
using MoonCore.Models;
using MoonCore.PluginFramework.Extensions;
using MoonCore.PluginFramework.Services;
using MoonCore.Services;
using Moonlight.ApiServer;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Database;
using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Helpers;
using Moonlight.ApiServer.Helpers.Authentication;
using Moonlight.ApiServer.Http.Middleware;
using Moonlight.ApiServer.Implementations.OAuth2;
using Moonlight.ApiServer.Interfaces.Auth;
using Moonlight.ApiServer.Interfaces.OAuth2;
using Moonlight.ApiServer.Interfaces.Startup;
// Prepare file system
Directory.CreateDirectory(PathBuilder.Dir("storage"));
Directory.CreateDirectory(PathBuilder.Dir("storage", "plugins"));
Directory.CreateDirectory(PathBuilder.Dir("storage", "clientPlugins"));
Directory.CreateDirectory(PathBuilder.Dir("storage", "logs"));
// Configuration
var configService = new ConfigService<AppConfiguration>(
PathBuilder.File("storage", "config.json")
);
var config = configService.Get();
ApplicationStateHelper.SetConfiguration(configService);
// Build pre run logger
var providers = LoggerBuildHelper.BuildFromConfiguration(configuration =>
{
configuration.Console.Enable = true;
configuration.Console.EnableAnsiMode = true;
configuration.FileLogging.Enable = true;
configuration.FileLogging.Path = PathBuilder.File("storage", "logs", "moonlight.log");
configuration.FileLogging.EnableLogRotation = true;
configuration.FileLogging.RotateLogNameTemplate = PathBuilder.File("storage", "logs", "moonlight.log.{0}");
});
using var loggerFactory = new LoggerFactory(providers);
var logger = loggerFactory.CreateLogger("Startup");
// Cry about it
#pragma warning disable ASP0000
// Fancy start console output... yes very fancy :>
var rainbow = new Crayon.Rainbow(0.5);
@@ -65,128 +29,92 @@ foreach (var c in "Moonlight")
Console.WriteLine();
var builder = WebApplication.CreateBuilder(args);
// Storage i guess
Directory.CreateDirectory(PathBuilder.Dir("storage"));
// Configure application logging
builder.Logging.ClearProviders();
builder.Logging.AddProviders(providers);
// Logging levels
var logConfigPath = PathBuilder.File("storage", "logConfig.json");
// Ensure logging config, add a default one is missing
if (!File.Exists(logConfigPath))
{
await File.WriteAllTextAsync(logConfigPath,
"{\"LogLevel\":{\"Default\":\"Information\",\"Microsoft.AspNetCore\":\"Warning\",\"MoonCore.Extended.Helpers.JwtHelper\": \"Error\"}}");
}
builder.Logging.AddConfiguration(await File.ReadAllTextAsync(logConfigPath));
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddSingleton(configService);
builder.Services.AutoAddServices<Program>();
// OAuth2
builder.Services.AddSingleton<TokenHelper>();
builder.Services.AddHttpClient();
builder.Services.AddOAuth2Consumer(configuration =>
{
configuration.ClientId = config.Authentication.OAuth2.ClientId;
configuration.ClientSecret = config.Authentication.OAuth2.ClientSecret;
configuration.AuthorizationRedirect =
config.Authentication.OAuth2.AuthorizationRedirect ?? $"{config.PublicUrl}/auth";
configuration.AccessEndpoint = config.Authentication.OAuth2.AccessEndpoint ?? $"{config.PublicUrl}/oauth2/access";
configuration.RefreshEndpoint = config.Authentication.OAuth2.RefreshEndpoint ?? $"{config.PublicUrl}/oauth2/refresh";
if (config.Authentication.UseLocalOAuth2)
{
configuration.AuthorizationEndpoint = config.Authentication.OAuth2.AuthorizationRedirect ?? $"{config.PublicUrl}/oauth2/authorize";
}
else
{
if(config.Authentication.OAuth2.AuthorizationUri == null)
logger.LogWarning("The 'AuthorizationUri' for the oauth2 client is not set. If you want to use an external oauth2 provider, you need to specify this url. If you want to use the local oauth2 service, set 'UseLocalOAuth2Service' to true");
configuration.AuthorizationEndpoint = config.Authentication.OAuth2.AuthorizationUri!;
}
});
if (config.Authentication.UseLocalOAuth2)
{
logger.LogInformation("Using local oauth2 provider");
builder.Services.AddOAuth2Provider(configuration =>
{
configuration.AccessSecret = config.Authentication.LocalOAuth2.AccessSecret;
configuration.RefreshSecret = config.Authentication.LocalOAuth2.RefreshSecret;
configuration.ClientId = config.Authentication.OAuth2.ClientId;
configuration.ClientSecret = config.Authentication.OAuth2.ClientSecret;
configuration.CodeSecret = config.Authentication.LocalOAuth2.CodeSecret;
configuration.AuthorizationRedirect =
config.Authentication.OAuth2.AuthorizationRedirect ?? $"{config.PublicUrl}/auth";
configuration.AccessTokenDuration = 60;
configuration.RefreshTokenDuration = 3600;
});
}
builder.Services.AddTokenAuthentication(configuration =>
{
configuration.AccessSecret = config.Authentication.AccessSecret;
configuration.DataLoader = async (data, provider, context) =>
{
if (!data.TryGetValue("userId", out var userIdStr) || !userIdStr.TryGetInt32(out var userId))
return false;
var userRepo = provider.GetRequiredService<DatabaseRepository<User>>();
var user = userRepo.Get().FirstOrDefault(x => x.Id == userId);
if (user == null)
return false;
// Load permissions, handle empty values
var permissions = JsonSerializer.Deserialize<string[]>(
string.IsNullOrEmpty(user.PermissionsJson) ? "[]" : user.PermissionsJson
) ?? [];
// Save permission state
context.User = new PermClaimsPrinciple(permissions, user);
return true;
};
});
// Database
var databaseHelper = new DatabaseHelper(
loggerFactory.CreateLogger<DatabaseHelper>()
// Configuration
var configService = new ConfigService<AppConfiguration>(
PathBuilder.File("storage", "config.json")
);
builder.Services.AddSingleton(databaseHelper);
builder.Services.AddScoped(typeof(DatabaseRepository<>));
builder.Services.AddScoped(typeof(CrudHelper<,>));
var config = configService.Get();
builder.Services.AddDbContext<CoreDataContext>();
databaseHelper.AddDbContext<CoreDataContext>();
ApplicationStateHelper.SetConfiguration(configService);
databaseHelper.GenerateMappings();
// TODO: Load plugin/module assemblies
// API Docs
if (configService.Get().Development.EnableApiDocs)
// Configure startup logger
var startupLoggerFactory = new LoggerFactory();
// TODO: Add direct extension method
var providers = LoggerBuildHelper.BuildFromConfiguration(configuration =>
{
// Configure swagger api specification generator and set the document title for the api docs to use
builder.Services.AddSwaggerGen(options => options.SwaggerDoc("main", new OpenApiInfo()
configuration.Console.Enable = true;
configuration.Console.EnableAnsiMode = true;
configuration.FileLogging.Enable = false;
});
startupLoggerFactory.AddProviders(providers);
var startupLogger = startupLoggerFactory.CreateLogger("Startup");
// Configure startup interfaces
var startupServiceCollection = new ServiceCollection();
startupServiceCollection.AddSingleton(configService);
startupServiceCollection.AddLogging(loggingBuilder => { loggingBuilder.AddProviders(providers); });
startupServiceCollection.AddPlugins(configuration =>
{
Title = "Moonlight API"
}));
// Configure startup interfaces
configuration.AddInterface<IAppStartup>();
configuration.AddInterface<IDatabaseStartup>();
configuration.AddInterface<IEndpointStartup>();
// Configure assemblies to scan
configuration.AddAssembly(Assembly.GetEntryAssembly()!);
}, startupLogger);
var startupServiceProvider = startupServiceCollection.BuildServiceProvider();
var appStartupInterfaces = startupServiceProvider.GetRequiredService<IAppStartup[]>();
// Start the actual app
var builder = WebApplication.CreateBuilder(args);
await Startup.ConfigureLogging(builder);
await Startup.ConfigureDatabase(
builder,
startupLoggerFactory,
startupServiceProvider.GetRequiredService<IDatabaseStartup[]>()
);
// Call interfaces
foreach (var startupInterface in appStartupInterfaces)
{
try
{
await startupInterface.BuildApp(builder);
}
catch (Exception e)
{
startupLogger.LogCritical(
"An unhandled error occured while processing BuildApp call for interface '{interfaceName}': {e}",
startupInterface.GetType().FullName,
e
);
}
}
builder.Services.AddControllers();
builder.Services.AddSingleton(configService);
builder.Services.AutoAddServices<Program>();
builder.Services.AddSingleton<TokenHelper>();
builder.Services.AddHttpClient();
await Startup.ConfigureTokenAuthentication(builder, config);
await Startup.ConfigureOAuth2(builder, startupLogger, config);
// Implementation service
builder.Services.AddPlugins(configuration =>
@@ -195,14 +123,11 @@ builder.Services.AddPlugins(configuration =>
configuration.AddInterface<IAuthInterceptor>();
configuration.AddAssembly(Assembly.GetEntryAssembly()!);
}, logger);
}, startupLogger);
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
await databaseHelper.EnsureMigrated(scope.ServiceProvider);
}
await Startup.PrepareDatabase(app);
if (app.Environment.IsDevelopment())
app.UseWebAssemblyDebugging();
@@ -214,16 +139,47 @@ app.UseRouting();
app.UseMiddleware<ApiErrorMiddleware>();
app.UseTokenAuthentication();
await Startup.UseTokenAuthentication(app);
// Call interfaces
foreach (var startupInterface in appStartupInterfaces)
{
try
{
await startupInterface.ConfigureApp(app);
}
catch (Exception e)
{
startupLogger.LogCritical(
"An unhandled error occured while processing ConfigureApp call for interface '{interfaceName}': {e}",
startupInterface.GetType().FullName,
e
);
}
}
app.UseMiddleware<AuthorizationMiddleware>();
app.MapControllers();
// Call interfaces
var endpointStartupInterfaces = startupServiceProvider.GetRequiredService<IEndpointStartup[]>();
foreach (var endpointStartup in endpointStartupInterfaces)
{
try
{
await endpointStartup.ConfigureEndpoints(app);
}
catch (Exception e)
{
startupLogger.LogCritical(
"An unhandled error occured while processing ConfigureEndpoints call for interface '{interfaceName}': {e}",
endpointStartup.GetType().FullName,
e
);
}
}
app.MapControllers();
app.MapFallbackToFile("index.html");
// API Docs
if (configService.Get().Development.EnableApiDocs)
app.MapSwagger("/api/swagger/{documentName}");
app.Run();

View File

@@ -0,0 +1,180 @@
using System.Text.Json;
using MoonCore.Extended.Abstractions;
using MoonCore.Extended.Extensions;
using MoonCore.Extended.Helpers;
using MoonCore.Extensions;
using MoonCore.Helpers;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Helpers;
using Moonlight.ApiServer.Helpers.Authentication;
using Moonlight.ApiServer.Interfaces.Startup;
namespace Moonlight.ApiServer;
public static class Startup
{
#region Logging
public static async Task ConfigureLogging(IHostApplicationBuilder builder)
{
// Configure application logging
builder.Logging.ClearProviders();
builder.Logging.AddMoonCore(configuration =>
{
configuration.Console.Enable = true;
configuration.Console.EnableAnsiMode = true;
configuration.FileLogging.Enable = true;
configuration.FileLogging.Path = PathBuilder.File("storage", "logs", "moonlight.log");
configuration.FileLogging.EnableLogRotation = true;
configuration.FileLogging.RotateLogNameTemplate = PathBuilder.File("storage", "logs", "moonlight.log.{0}");
});
// Logging levels
var logConfigPath = PathBuilder.File("storage", "logConfig.json");
// Ensure logging config, add a default one is missing
if (!File.Exists(logConfigPath))
{
await File.WriteAllTextAsync(logConfigPath,
"{\"LogLevel\":{\"Default\":\"Information\",\"Microsoft.AspNetCore\":\"Warning\",\"MoonCore.Extended.Helpers.JwtHelper\": \"Error\"}}");
}
builder.Logging.AddConfiguration(await File.ReadAllTextAsync(logConfigPath));
}
#endregion
#region Token Authentication
public static Task ConfigureTokenAuthentication(IHostApplicationBuilder builder, AppConfiguration config)
{
builder.Services.AddTokenAuthentication(configuration =>
{
configuration.AccessSecret = config.Authentication.AccessSecret;
configuration.DataLoader = async (data, provider, context) =>
{
if (!data.TryGetValue("userId", out var userIdStr) || !userIdStr.TryGetInt32(out var userId))
return false;
var userRepo = provider.GetRequiredService<DatabaseRepository<User>>();
var user = userRepo.Get().FirstOrDefault(x => x.Id == userId);
if (user == null)
return false;
// Load permissions, handle empty values
var permissions = JsonSerializer.Deserialize<string[]>(
string.IsNullOrEmpty(user.PermissionsJson) ? "[]" : user.PermissionsJson
) ?? [];
// Save permission state
context.User = new PermClaimsPrinciple(permissions, user);
return true;
};
});
return Task.CompletedTask;
}
public static Task UseTokenAuthentication(IApplicationBuilder builder)
{
builder.UseTokenAuthentication();
return Task.CompletedTask;
}
#endregion
#region Database
public static async Task ConfigureDatabase(IHostApplicationBuilder builder, ILoggerFactory loggerFactory,
IDatabaseStartup[] databaseStartups)
{
var logger = loggerFactory.CreateLogger<DatabaseHelper>();
var databaseHelper = new DatabaseHelper(logger);
var databaseCollection = new DatabaseContextCollection();
foreach (var databaseStartup in databaseStartups)
await databaseStartup.ConfigureDatabase(databaseCollection);
foreach (var database in databaseCollection)
{
databaseHelper.AddDbContext(database);
builder.Services.AddScoped(database);
}
databaseHelper.GenerateMappings();
builder.Services.AddSingleton(databaseHelper);
builder.Services.AddScoped(typeof(DatabaseRepository<>));
builder.Services.AddScoped(typeof(CrudHelper<,>));
}
public static async Task PrepareDatabase(IApplicationBuilder builder)
{
using var scope = builder.ApplicationServices.CreateScope();
var databaseHelper = scope.ServiceProvider.GetRequiredService<DatabaseHelper>();
await databaseHelper.EnsureMigrated(scope.ServiceProvider);
}
#endregion
#region OAuth2
public static Task ConfigureOAuth2(IHostApplicationBuilder builder, ILogger logger, AppConfiguration config)
{
builder.Services.AddOAuth2Consumer(configuration =>
{
configuration.ClientId = config.Authentication.OAuth2.ClientId;
configuration.ClientSecret = config.Authentication.OAuth2.ClientSecret;
configuration.AuthorizationRedirect =
config.Authentication.OAuth2.AuthorizationRedirect ?? $"{config.PublicUrl}/auth";
configuration.AccessEndpoint =
config.Authentication.OAuth2.AccessEndpoint ?? $"{config.PublicUrl}/oauth2/access";
configuration.RefreshEndpoint =
config.Authentication.OAuth2.RefreshEndpoint ?? $"{config.PublicUrl}/oauth2/refresh";
if (config.Authentication.UseLocalOAuth2)
{
configuration.AuthorizationEndpoint = config.Authentication.OAuth2.AuthorizationRedirect ??
$"{config.PublicUrl}/oauth2/authorize";
}
else
{
if (config.Authentication.OAuth2.AuthorizationUri == null)
logger.LogWarning(
"The 'AuthorizationUri' for the oauth2 client is not set. If you want to use an external oauth2 provider, you need to specify this url. If you want to use the local oauth2 service, set 'UseLocalOAuth2Service' to true");
configuration.AuthorizationEndpoint = config.Authentication.OAuth2.AuthorizationUri!;
}
});
if (!config.Authentication.UseLocalOAuth2) return Task.CompletedTask;
logger.LogInformation("Using local oauth2 provider");
builder.Services.AddOAuth2Provider(configuration =>
{
configuration.AccessSecret = config.Authentication.LocalOAuth2.AccessSecret;
configuration.RefreshSecret = config.Authentication.LocalOAuth2.RefreshSecret;
configuration.ClientId = config.Authentication.OAuth2.ClientId;
configuration.ClientSecret = config.Authentication.OAuth2.ClientSecret;
configuration.CodeSecret = config.Authentication.LocalOAuth2.CodeSecret;
configuration.AuthorizationRedirect =
config.Authentication.OAuth2.AuthorizationRedirect ?? $"{config.PublicUrl}/auth";
configuration.AccessTokenDuration = 60;
configuration.RefreshTokenDuration = 3600;
});
return Task.CompletedTask;
}
#endregion
}

0
Moonlight.Client/Styles/mappings/mooncore.map Normal file → Executable file
View File