Make the Worker validate its service container and align Worker and Gateway hosts

The Worker now registers the Application layer, observability and stream tickets, validates the
container on build and ships a complete appsettings. The Gateway drops underscore fields and uses a
placeholder bucket in its default YARP transform.
This commit is contained in:
2026-09-24 12:40:18 +02:00
parent fcf3093ad8
commit c4cf5af7f1
10 changed files with 92 additions and 52 deletions
@@ -14,8 +14,8 @@ public static partial class MassTransitDependencyInjection
public static IServiceCollection AddMassTransitClient(this IServiceCollection services, IConfiguration configuration)
{
services.AddValidatedOptions<MassTransitConfiguration>(configuration);
services.AddScoped<IEventBus, MassTransitEventBus>();
services.AddMassTransit(options =>
{
AddOutbox(options);
@@ -34,6 +34,7 @@ public static partial class MassTransitDependencyInjection
services.AddValidatedOptions<MassTransitConfiguration>(configuration);
services.AddValidatedOptions<WorkerConfiguration>(configuration);
services.AddScoped<IEventBus, MassTransitEventBus>();
RegisterRoutingSlipBuilders(services);
@@ -38,13 +38,6 @@ public static class ServicesDependencyInjection
return services;
}
public static IServiceCollection AddUploadIntentConfiguration(this IServiceCollection services, IConfiguration configuration)
{
services.AddValidatedOptions<UploadIntentConfiguration>(configuration);
return services;
}
public static IServiceCollection AddJobs(this IServiceCollection services)
{
services.AddHangfire((serviceProvider, hangfireConfiguration) =>
@@ -14,27 +14,24 @@ public sealed record TicketValidationResult(string Prefix, long? MaxBytes)
public sealed class TicketValidator : IDisposable
{
private readonly StreamTicketValidationConfiguration _options;
private readonly RSA _rsa;
private readonly TokenValidationParameters _validationParameters;
private readonly JsonWebTokenHandler _handler = new();
private readonly RSA rsa;
private readonly TokenValidationParameters validationParameters;
private readonly JsonWebTokenHandler handler = new();
public TicketValidator(StreamTicketValidationConfiguration options)
public TicketValidator(StreamTicketValidationConfiguration configuration)
{
_options = options;
rsa = RSA.Create();
rsa.ImportFromPem(File.ReadAllText(configuration.PublicKeyPath));
_rsa = RSA.Create();
_rsa.ImportFromPem(File.ReadAllText(_options.PublicKeyPath));
_validationParameters = new TokenValidationParameters
validationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = _options.Issuer,
ValidIssuer = configuration.Issuer,
ValidateAudience = true,
ValidAudience = _options.Audience,
ValidAudience = configuration.Audience,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new RsaSecurityKey(_rsa),
IssuerSigningKey = new RsaSecurityKey(rsa),
ValidAlgorithms = [SecurityAlgorithms.RsaSha256],
ClockSkew = TimeSpan.FromSeconds(30),
};
@@ -45,7 +42,7 @@ public sealed class TicketValidator : IDisposable
if (string.IsNullOrWhiteSpace(token))
return TicketValidationResult.Invalid;
var result = await _handler.ValidateTokenAsync(token, _validationParameters);
var result = await handler.ValidateTokenAsync(token, validationParameters);
if (!result.IsValid)
return TicketValidationResult.Invalid;
@@ -59,5 +56,5 @@ public sealed class TicketValidator : IDisposable
return new TicketValidationResult(prefix, maxBytes);
}
public void Dispose() => _rsa.Dispose();
public void Dispose() => rsa.Dispose();
}
@@ -3,7 +3,9 @@ using Musify.StreamingGateway.Authentication;
using Musify.StreamingGateway.Configuration;
using Musify.StreamingGateway.Middleware;
var builder = WebApplication.CreateBuilder();
const string CorsPolicy = "media-clients";
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddOptions<StreamTicketValidationConfiguration>()
@@ -16,9 +18,8 @@ builder.Services.AddSingleton(serviceProvider =>
builder.Services.AddSingleton<TicketValidator>();
const string corsPolicy = "media-clients";
var allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() ?? [];
builder.Services.AddCors(options => options.AddPolicy(corsPolicy, policy =>
builder.Services.AddCors(options => options.AddPolicy(CorsPolicy, policy =>
{
if (builder.Environment.IsDevelopment())
policy.AllowAnyOrigin();
@@ -37,9 +38,9 @@ builder.Services
var app = builder.Build();
app.MapGet("/health", () => Results.Ok("healthy"))
.WithSummary("Health Check Endpoint");
.WithSummary("Health check endpoint");
app.UseCors(corsPolicy);
app.UseCors(CorsPolicy);
app.UseMiddleware<TicketValidationMiddleware>();
app.MapReverseProxy();
@@ -1,5 +1,14 @@
{
"StreamTicket": {
"PublicKeyPath": "../../../deploy/keys/stream_public.pem"
},
"ReverseProxy": {
"Routes": {
"media": {
"Transforms": [
{ "PathPattern": "/buckets/webapi-storage/{**rest}" }
]
}
}
}
}
@@ -26,7 +26,7 @@
"ClusterId": "seaweed-filer",
"Match": { "Path": "/media/{**rest}" },
"Transforms": [
{ "PathPattern": "/buckets/webapi-storage/{**rest}" }
{ "PathPattern": "/buckets/CHANGE_ME/{**rest}" }
]
}
},
+14 -19
View File
@@ -1,32 +1,27 @@
using Musify.Application.Configuration;
using Musify.Application;
using Musify.Infrastructure.MassTransit;
using Musify.Infrastructure.Observability;
using Musify.Infrastructure.Persistence;
using Musify.Infrastructure.Services;
var builder = Host.CreateApplicationBuilder();
var builder = Host.CreateApplicationBuilder(args);
builder.ConfigureContainer(new DefaultServiceProviderFactory(new ServiceProviderOptions
{
ValidateOnBuild = false,
ValidateOnBuild = true,
ValidateScopes = true
}));
var configuration = builder.Configuration;
var services = builder.Services;
services.AddValidatedOptions<ApplicationStorageConfiguration>(configuration);
services.AddValidatedOptions<PlayListConfiguration>(configuration);
services.AddValidatedOptions<TrackConfiguration>(configuration);
services.AddValidatedOptions<MixConfiguration>(configuration);
services.AddStorageService(configuration);
services.AddAudioTranscoder(configuration);
services.AddPictureService();
services.AddDatabase(configuration);
services.AddMassTransitConsumers(configuration);
services.AddUploadIntentConfiguration(configuration);
services.AddMediator();
services.AddJobs();
builder.Services
.AddApplication(builder.Configuration)
.AddObservability(builder.Configuration)
.AddDatabase(builder.Configuration)
.AddStorageService(builder.Configuration)
.AddAudioTranscoder(builder.Configuration)
.AddStreamTicketService(builder.Configuration)
.AddPictureService()
.AddMassTransitConsumers(builder.Configuration)
.AddJobs();
var host = builder.Build();
@@ -26,10 +26,11 @@
"ApplicationStorage": {
"Bucket": "webapi-storage"
},
"AudioTranscoder": {
"Routes": {
"WorkingDirectory": "../../../deploy/.tmp"
}
"StreamGateway": {
"PublicBaseUrl": "http://localhost:8081"
},
"StreamTicket": {
"PrivateKeyPath": "../../../deploy/keys/stream_private.pem"
},
"Workers": {
"Routes": {
@@ -26,11 +26,27 @@
"ApplicationStorage": {
"Bucket": ""
},
"StreamGateway": {
"PublicBaseUrl": "",
"AudioFileName": "audio.m4a"
},
"StreamTicket": {
"PrivateKeyPath": "",
"Audience": "media-gateway",
"Issuer": "musify-webapi",
"TicketTtlSeconds": 3600
},
"Playback": {
"AllowAnonymousListening": false,
"AnonymousFragmentSeconds": 0,
"EstimatedAudioBytesPerSecond": 16000
},
"Mix": {
"ItemsPerMix": 12
},
"PlayList": {
"Routes": {
"UploadsFolder": "uploads",
"ParentFolder": "PlayLists",
"OriginalPicturesFolder": "OriginalPictures",
"SmallPicturesFolder": "SmallPictures",
@@ -46,8 +62,27 @@
"LargeHeight": 512
}
},
"Album": {
"Routes": {
"UploadsFolder": "uploads",
"ParentFolder": "Albums",
"OriginalPicturesFolder": "OriginalPictures",
"SmallPicturesFolder": "SmallPictures",
"MediumPicturesFolder": "MediumPictures",
"LargePicturesFolder": "LargePictures"
},
"PicturesSizes": {
"SmallWidth": 128,
"SmallHeight": 128,
"MediumWidth": 256,
"MediumHeight": 256,
"LargeWidth": 512,
"LargeHeight": 512
}
},
"Track": {
"Routes": {
"UploadsFolder": "uploads",
"ParentFolder": "Tracks",
"OriginalPicturesFolder": "OriginalPictures",
"SmallPicturesFolder": "SmallPictures",