diff --git a/.gitignore b/.gitignore index 9491a2f..4e267a7 100644 --- a/.gitignore +++ b/.gitignore @@ -360,4 +360,7 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd + +# Environment +.env \ No newline at end of file diff --git a/JSMR.Api/Program.cs b/JSMR.Api/Program.cs index a1a476c..66c609e 100644 --- a/JSMR.Api/Program.cs +++ b/JSMR.Api/Program.cs @@ -2,13 +2,16 @@ using JSMR.Api.Startup; WebApplicationBuilder builder = WebApplication.CreateBuilder(args); +ConfigurationManager configuration = builder.Configuration; +IWebHostEnvironment environment = builder.Environment; + builder.Services - .AddAppServices(builder) + .AddAppServices(configuration) .AddAppJson() .AddAppOpenApi() - .AddAppAuthentication() - .AddAppCors(builder) - .AddAppLogging(builder); + .AddAppAuthentication(environment) + .AddAppCors(configuration); + //.AddAppLogging(builder); builder.Host.UseAppSerilog(); diff --git a/JSMR.Api/Startup/HostBuilderExtensions.cs b/JSMR.Api/Startup/HostBuilderExtensions.cs index 1466f7b..6dc947e 100644 --- a/JSMR.Api/Startup/HostBuilderExtensions.cs +++ b/JSMR.Api/Startup/HostBuilderExtensions.cs @@ -1,9 +1,31 @@ using Serilog; +using Serilog.Events; namespace JSMR.Api.Startup; public static class HostBuilderExtensions { public static IHostBuilder UseAppSerilog(this IHostBuilder host) - => host.UseSerilog(); + { + return host.UseSerilog((context, services, loggerConfiguration) => + { + IConfiguration configuration = context.Configuration; + IHostEnvironment environment = context.HostingEnvironment; + + loggerConfiguration + .ReadFrom.Configuration(configuration) + .ReadFrom.Services(services) + .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) + .Enrich.WithProperty("Service", "JSMR.Api") + .Enrich.WithProperty("Environment", environment.EnvironmentName); + + // Conditionally add Seq if configured correctly + string? seqUrl = configuration["Seq:ServerUrl"]; + + if (Uri.TryCreate(seqUrl, UriKind.Absolute, out _)) + { + loggerConfiguration.WriteTo.Seq(seqUrl); + } + }); + } } \ No newline at end of file diff --git a/JSMR.Api/Startup/ServiceCollectionExtensions.cs b/JSMR.Api/Startup/ServiceCollectionExtensions.cs index dc52255..1ed30ad 100644 --- a/JSMR.Api/Startup/ServiceCollectionExtensions.cs +++ b/JSMR.Api/Startup/ServiceCollectionExtensions.cs @@ -4,8 +4,6 @@ using JSMR.Infrastructure.DI; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Http.Json; using Microsoft.EntityFrameworkCore; -using Serilog; -using Serilog.Events; using System.Text.Json; using System.Text.Json.Serialization; @@ -13,14 +11,14 @@ namespace JSMR.Api.Startup; public static class ServiceCollectionExtensions { - public static IServiceCollection AddAppServices(this IServiceCollection services, IHostApplicationBuilder builder) + public static IServiceCollection AddAppServices(this IServiceCollection services, IConfigurationManager configuration) { services .AddMemoryCache() .AddApplication() .AddInfrastructure(); - string connectionString = builder.Configuration.GetConnectionString("AppDb") + string connectionString = configuration.GetConnectionString("AppDb") ?? throw new InvalidOperationException("Missing ConnectionStrings:AppDb"); services.AddDbContextFactory(opt => @@ -51,7 +49,7 @@ public static class ServiceCollectionExtensions return services; } - public static IServiceCollection AddAppAuthentication(this IServiceCollection services) + public static IServiceCollection AddAppAuthentication(this IServiceCollection services, IHostEnvironment environment) { services.AddAuthorization(); @@ -61,8 +59,15 @@ public static class ServiceCollectionExtensions { options.Cookie.Name = "vw_auth"; options.Cookie.HttpOnly = true; - options.Cookie.SameSite = SameSiteMode.None; - options.Cookie.SecurePolicy = CookieSecurePolicy.Always; + //options.Cookie.SameSite = SameSiteMode.None; + //options.Cookie.SecurePolicy = CookieSecurePolicy.Always; + + options.Cookie.SameSite = SameSiteMode.Lax; + + options.Cookie.SecurePolicy = + environment.IsDevelopment() + ? CookieSecurePolicy.SameAsRequest + : CookieSecurePolicy.Always; options.Events = new CookieAuthenticationEvents { @@ -82,37 +87,44 @@ public static class ServiceCollectionExtensions return services; } - public static IServiceCollection AddAppCors(this IServiceCollection services, IHostApplicationBuilder builder) + public static IServiceCollection AddAppCors(this IServiceCollection services, IConfigurationManager configuration) { - // Prefer config-based origins so you stop editing code for ports. - // appsettings.Development.json: - // "Cors": { "AllowedOrigins": [ "https://localhost:7112", ... ] } - string[] origins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get() ?? []; + string[] origins = configuration.GetSection("Cors:AllowedOrigins").Get() ?? []; services.AddCors(options => { options.AddPolicy("ui", policyBuilder => + { + if (origins.Length == 0) + { + // In container/prod you often don't need CORS at all (same-origin), + // but if it *is* needed and not configured, fail closed rather than crash. + // Do not call WithOrigins(). + return; + } + policyBuilder.WithOrigins(origins) .AllowAnyHeader() .AllowAnyMethod() - .AllowCredentials()); + .AllowCredentials(); + }); }); return services; } - public static IServiceCollection AddAppLogging(this IServiceCollection services, IHostApplicationBuilder builder) - { - var config = builder.Configuration; - var env = builder.Environment; + //public static IServiceCollection AddAppLogging(this IServiceCollection services, IHostApplicationBuilder builder) + //{ + // var config = builder.Configuration; + // var env = builder.Environment; - Log.Logger = new LoggerConfiguration() - .ReadFrom.Configuration(config) - .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) - .Enrich.WithProperty("Service", "JSMR.Api") - .Enrich.WithProperty("Environment", env.EnvironmentName) - .CreateLogger(); + // Log.Logger = new LoggerConfiguration() + // .ReadFrom.Configuration(config) + // .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) + // .Enrich.WithProperty("Service", "JSMR.Api") + // .Enrich.WithProperty("Environment", env.EnvironmentName) + // .CreateLogger(); - return services; - } + // return services; + //} } \ No newline at end of file diff --git a/JSMR.Api/Startup/WebApplicationExtensions.cs b/JSMR.Api/Startup/WebApplicationExtensions.cs index b2d74c0..c9fb08e 100644 --- a/JSMR.Api/Startup/WebApplicationExtensions.cs +++ b/JSMR.Api/Startup/WebApplicationExtensions.cs @@ -15,12 +15,18 @@ public static class WebApplicationExtensions { public static WebApplication UseAppPipeline(this WebApplication app, IHostEnvironment env) { - app.UseCors("ui"); + string[] origins = app.Configuration.GetSection("Cors:AllowedOrigins").Get() ?? []; + + if (origins.Length > 0) + app.UseCors("ui"); if (env.IsDevelopment()) app.MapOpenApi(); - app.UseHttpsRedirection(); + if (!env.IsDevelopment()) + { + app.UseHttpsRedirection(); + } app.UseAuthentication(); app.UseAuthorization(); diff --git a/JSMR.Api/appsettings.json b/JSMR.Api/appsettings.json index 93ce949..a6e3d68 100644 --- a/JSMR.Api/appsettings.json +++ b/JSMR.Api/appsettings.json @@ -25,11 +25,10 @@ }, "Enrich": [ "FromLogContext", "WithMachineName", "WithProcessId", "WithThreadId" ], "WriteTo": [ - { "Name": "Console" }, - { - "Name": "Seq", - "Args": { "serverUrl": "%SEQ_URL%" } - } + { "Name": "Console" } ] + }, + "Seq": { + "ServerUrl": "" } } diff --git a/JSMR.UI.Blazor/Dockerfile b/JSMR.UI.Blazor/Dockerfile new file mode 100644 index 0000000..84f2226 --- /dev/null +++ b/JSMR.UI.Blazor/Dockerfile @@ -0,0 +1,8 @@ +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build +WORKDIR /app +COPY . . +RUN dotnet publish -c Release -o out + +FROM nginx:alpine +COPY --from=build /app/out/wwwroot /usr/share/nginx/html +COPY JSMR.UI.Blazor/nginx.conf /etc/nginx/conf.d/default.conf \ No newline at end of file diff --git a/JSMR.UI.Blazor/Program.cs b/JSMR.UI.Blazor/Program.cs index 3653a5c..2edcfde 100644 --- a/JSMR.UI.Blazor/Program.cs +++ b/JSMR.UI.Blazor/Program.cs @@ -10,9 +10,14 @@ var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add("#app"); builder.RootComponents.Add("head::after"); -string apiBase = builder.Configuration["ApiBaseUrl"] ?? builder.HostEnvironment.BaseAddress; +//string apiBase = builder.Configuration["ApiBaseUrl"] ?? builder.HostEnvironment.BaseAddress; +//Console.WriteLine(apiBase); -Console.WriteLine(apiBase); +// If ApiBaseUrl is set (VS dev), use it. Otherwise (docker/prod), use same-origin. +var apiBase = builder.Configuration["ApiBaseUrl"]; + +if (string.IsNullOrWhiteSpace(apiBase)) + apiBase = builder.HostEnvironment.BaseAddress; // Old way //builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(apiBase) }); diff --git a/JSMR.UI.Blazor/nginx.conf b/JSMR.UI.Blazor/nginx.conf new file mode 100644 index 0000000..bec8e53 --- /dev/null +++ b/JSMR.UI.Blazor/nginx.conf @@ -0,0 +1,32 @@ +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + # Blazor WASM: serve static files, and fallback to index.html for client-side routes + location / { + try_files $uri $uri/ /index.html; + } + + # Proxy API calls to the api service (docker-compose DNS name: api) + location /api/ { + proxy_pass http://api:8080; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Proxy auth endpoints too (yours are /auth/login, /auth/logout) + location /auth/ { + proxy_pass http://api:8080; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} \ No newline at end of file diff --git a/JSMR.UI.Blazor/wwwroot/appsettings.Development.json b/JSMR.UI.Blazor/wwwroot/appsettings.Development.json new file mode 100644 index 0000000..300ae79 --- /dev/null +++ b/JSMR.UI.Blazor/wwwroot/appsettings.Development.json @@ -0,0 +1,3 @@ +{ + "ApiBaseUrl": "https://localhost:7277" +} \ No newline at end of file diff --git a/JSMR.UI.Blazor/wwwroot/appsettings.json b/JSMR.UI.Blazor/wwwroot/appsettings.json index 300ae79..62a9192 100644 --- a/JSMR.UI.Blazor/wwwroot/appsettings.json +++ b/JSMR.UI.Blazor/wwwroot/appsettings.json @@ -1,3 +1,3 @@ { - "ApiBaseUrl": "https://localhost:7277" + "ApiBaseUrl": "" } \ No newline at end of file diff --git a/JSMR.sln b/JSMR.sln index 3d57d1a..e5eda41 100644 --- a/JSMR.sln +++ b/JSMR.sln @@ -17,6 +17,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JSMR.UI.Blazor", "JSMR.UI.B EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JSMR.Worker", "JSMR.Worker\JSMR.Worker.csproj", "{964BD375-FAE3-4044-A09B-5C43919C9B52}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F3C045AD-3861-4079-85F0-EDEEE83765B7}" + ProjectSection(SolutionItems) = preProject + docker-compose.yml = docker-compose.yml + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..36fad69 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,27 @@ +services: + api: + build: + context: . + dockerfile: JSMR.Api/Dockerfile + ports: + - "5000:8080" + environment: + ASPNETCORE_ENVIRONMENT: ${ASPNETCORE_ENVIRONMENT:-Production} + ConnectionStrings__AppDb: ${APPDB_CONN} + Seq__ServerUrl: ${SEQ_URL:-} + networks: + - app-net + + web: + build: + context: . + dockerfile: JSMR.UI.Blazor/Dockerfile + ports: + - "5001:80" + depends_on: + - api + networks: + - app-net + +networks: + app-net: {} \ No newline at end of file