diff --git a/backend/Core/Musify.Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs b/backend/Core/Musify.Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs index 8ed3d83..f5ec69a 100644 --- a/backend/Core/Musify.Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs +++ b/backend/Core/Musify.Infrastructure/MassTransit/DependencyInjection/MassTransitDependencyInjection.cs @@ -14,8 +14,8 @@ public static partial class MassTransitDependencyInjection public static IServiceCollection AddMassTransitClient(this IServiceCollection services, IConfiguration configuration) { services.AddValidatedOptions(configuration); - services.AddScoped(); + services.AddMassTransit(options => { AddOutbox(options); @@ -34,6 +34,7 @@ public static partial class MassTransitDependencyInjection services.AddValidatedOptions(configuration); services.AddValidatedOptions(configuration); + services.AddScoped(); RegisterRoutingSlipBuilders(services); diff --git a/backend/Core/Musify.Infrastructure/Services/ServicesDependencyInjection.cs b/backend/Core/Musify.Infrastructure/Services/ServicesDependencyInjection.cs index 0ad4da5..47e9c43 100644 --- a/backend/Core/Musify.Infrastructure/Services/ServicesDependencyInjection.cs +++ b/backend/Core/Musify.Infrastructure/Services/ServicesDependencyInjection.cs @@ -38,13 +38,6 @@ public static class ServicesDependencyInjection return services; } - public static IServiceCollection AddUploadIntentConfiguration(this IServiceCollection services, IConfiguration configuration) - { - services.AddValidatedOptions(configuration); - - return services; - } - public static IServiceCollection AddJobs(this IServiceCollection services) { services.AddHangfire((serviceProvider, hangfireConfiguration) => diff --git a/backend/Hosts/Musify.StreamingGateway/Authentication/TicketValidator.cs b/backend/Hosts/Musify.StreamingGateway/Authentication/TicketValidator.cs index 5782c4b..3a27213 100644 --- a/backend/Hosts/Musify.StreamingGateway/Authentication/TicketValidator.cs +++ b/backend/Hosts/Musify.StreamingGateway/Authentication/TicketValidator.cs @@ -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(); } diff --git a/backend/Hosts/Musify.StreamingGateway/Program.cs b/backend/Hosts/Musify.StreamingGateway/Program.cs index 2997e5d..c127bd7 100644 --- a/backend/Hosts/Musify.StreamingGateway/Program.cs +++ b/backend/Hosts/Musify.StreamingGateway/Program.cs @@ -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() @@ -16,9 +18,8 @@ builder.Services.AddSingleton(serviceProvider => builder.Services.AddSingleton(); -const string corsPolicy = "media-clients"; var allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get() ?? []; -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(); app.MapReverseProxy(); diff --git a/backend/Hosts/Musify.StreamingGateway/appsettings.Development.json b/backend/Hosts/Musify.StreamingGateway/appsettings.Development.json index 0bac6c4..9a8d81e 100644 --- a/backend/Hosts/Musify.StreamingGateway/appsettings.Development.json +++ b/backend/Hosts/Musify.StreamingGateway/appsettings.Development.json @@ -1,5 +1,14 @@ { "StreamTicket": { "PublicKeyPath": "../../../deploy/keys/stream_public.pem" + }, + "ReverseProxy": { + "Routes": { + "media": { + "Transforms": [ + { "PathPattern": "/buckets/webapi-storage/{**rest}" } + ] + } + } } } diff --git a/backend/Hosts/Musify.StreamingGateway/appsettings.json b/backend/Hosts/Musify.StreamingGateway/appsettings.json index a2eeaa5..52df1f0 100644 --- a/backend/Hosts/Musify.StreamingGateway/appsettings.json +++ b/backend/Hosts/Musify.StreamingGateway/appsettings.json @@ -26,7 +26,7 @@ "ClusterId": "seaweed-filer", "Match": { "Path": "/media/{**rest}" }, "Transforms": [ - { "PathPattern": "/buckets/webapi-storage/{**rest}" } + { "PathPattern": "/buckets/CHANGE_ME/{**rest}" } ] } }, diff --git a/backend/Hosts/Musify.Worker/Program.cs b/backend/Hosts/Musify.Worker/Program.cs index 0ea3523..f377677 100644 --- a/backend/Hosts/Musify.Worker/Program.cs +++ b/backend/Hosts/Musify.Worker/Program.cs @@ -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(configuration); -services.AddValidatedOptions(configuration); -services.AddValidatedOptions(configuration); -services.AddValidatedOptions(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(); diff --git a/backend/Hosts/Musify.Worker/appsettings.Development.json b/backend/Hosts/Musify.Worker/appsettings.Development.json index 3b8dee5..9ff1979 100644 --- a/backend/Hosts/Musify.Worker/appsettings.Development.json +++ b/backend/Hosts/Musify.Worker/appsettings.Development.json @@ -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": { diff --git a/backend/Hosts/Musify.Worker/appsettings.json b/backend/Hosts/Musify.Worker/appsettings.json index b7f7360..d4fccda 100644 --- a/backend/Hosts/Musify.Worker/appsettings.json +++ b/backend/Hosts/Musify.Worker/appsettings.json @@ -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", diff --git a/deploy/compose.yml b/deploy/compose.yml index 9d7d976..abcb832 100644 --- a/deploy/compose.yml +++ b/deploy/compose.yml @@ -340,10 +340,18 @@ services: depends_on: migrate: condition: service_completed_successfully + keys-init: + condition: service_completed_successfully environment: <<: *backend-env DOTNET_ENVIRONMENT: ${DOTNET_ENVIRONMENT} + # The Application handlers need these to build their dependencies; the + # Worker never issues stream tickets, so the key is never read. + StreamGateway__PublicBaseUrl: ${PUBLIC_STREAM_URL} + StreamTicket__PrivateKeyPath: /keys/stream_private.pem Workers__Routes__TemporaryFilesDirectory: /tmp/musify + volumes: + - ./keys:/keys:ro restart: unless-stopped # YARP in front of the SeaweedFS filer: validates the RS256 stream ticket and