Added docker-compose. Updated startups for API and Web layer.
Some checks failed
ci / build-test (push) Has been cancelled
ci / publish-image (push) Has been cancelled

This commit is contained in:
2026-02-24 00:25:03 -05:00
parent 80ca1296e5
commit ab3524ea20
13 changed files with 166 additions and 41 deletions

3
.gitignore vendored
View File

@@ -361,3 +361,6 @@ MigrationBackup/
# Fody - auto-generated XML schema # Fody - auto-generated XML schema
FodyWeavers.xsd FodyWeavers.xsd
# Environment
.env

View File

@@ -2,13 +2,16 @@ using JSMR.Api.Startup;
WebApplicationBuilder builder = WebApplication.CreateBuilder(args); WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
ConfigurationManager configuration = builder.Configuration;
IWebHostEnvironment environment = builder.Environment;
builder.Services builder.Services
.AddAppServices(builder) .AddAppServices(configuration)
.AddAppJson() .AddAppJson()
.AddAppOpenApi() .AddAppOpenApi()
.AddAppAuthentication() .AddAppAuthentication(environment)
.AddAppCors(builder) .AddAppCors(configuration);
.AddAppLogging(builder); //.AddAppLogging(builder);
builder.Host.UseAppSerilog(); builder.Host.UseAppSerilog();

View File

@@ -1,9 +1,31 @@
using Serilog; using Serilog;
using Serilog.Events;
namespace JSMR.Api.Startup; namespace JSMR.Api.Startup;
public static class HostBuilderExtensions public static class HostBuilderExtensions
{ {
public static IHostBuilder UseAppSerilog(this IHostBuilder host) 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);
}
});
}
} }

View File

@@ -4,8 +4,6 @@ using JSMR.Infrastructure.DI;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http.Json; using Microsoft.AspNetCore.Http.Json;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Serilog;
using Serilog.Events;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
@@ -13,14 +11,14 @@ namespace JSMR.Api.Startup;
public static class ServiceCollectionExtensions public static class ServiceCollectionExtensions
{ {
public static IServiceCollection AddAppServices(this IServiceCollection services, IHostApplicationBuilder builder) public static IServiceCollection AddAppServices(this IServiceCollection services, IConfigurationManager configuration)
{ {
services services
.AddMemoryCache() .AddMemoryCache()
.AddApplication() .AddApplication()
.AddInfrastructure(); .AddInfrastructure();
string connectionString = builder.Configuration.GetConnectionString("AppDb") string connectionString = configuration.GetConnectionString("AppDb")
?? throw new InvalidOperationException("Missing ConnectionStrings:AppDb"); ?? throw new InvalidOperationException("Missing ConnectionStrings:AppDb");
services.AddDbContextFactory<AppDbContext>(opt => services.AddDbContextFactory<AppDbContext>(opt =>
@@ -51,7 +49,7 @@ public static class ServiceCollectionExtensions
return services; return services;
} }
public static IServiceCollection AddAppAuthentication(this IServiceCollection services) public static IServiceCollection AddAppAuthentication(this IServiceCollection services, IHostEnvironment environment)
{ {
services.AddAuthorization(); services.AddAuthorization();
@@ -61,8 +59,15 @@ public static class ServiceCollectionExtensions
{ {
options.Cookie.Name = "vw_auth"; options.Cookie.Name = "vw_auth";
options.Cookie.HttpOnly = true; options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.None; //options.Cookie.SameSite = SameSiteMode.None;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always; //options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Lax;
options.Cookie.SecurePolicy =
environment.IsDevelopment()
? CookieSecurePolicy.SameAsRequest
: CookieSecurePolicy.Always;
options.Events = new CookieAuthenticationEvents options.Events = new CookieAuthenticationEvents
{ {
@@ -82,37 +87,44 @@ public static class ServiceCollectionExtensions
return services; 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. string[] origins = configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() ?? [];
// appsettings.Development.json:
// "Cors": { "AllowedOrigins": [ "https://localhost:7112", ... ] }
string[] origins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() ?? [];
services.AddCors(options => services.AddCors(options =>
{ {
options.AddPolicy("ui", policyBuilder => 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) policyBuilder.WithOrigins(origins)
.AllowAnyHeader() .AllowAnyHeader()
.AllowAnyMethod() .AllowAnyMethod()
.AllowCredentials()); .AllowCredentials();
});
}); });
return services; return services;
} }
public static IServiceCollection AddAppLogging(this IServiceCollection services, IHostApplicationBuilder builder) //public static IServiceCollection AddAppLogging(this IServiceCollection services, IHostApplicationBuilder builder)
{ //{
var config = builder.Configuration; // var config = builder.Configuration;
var env = builder.Environment; // var env = builder.Environment;
Log.Logger = new LoggerConfiguration() // Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(config) // .ReadFrom.Configuration(config)
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning) // .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.Enrich.WithProperty("Service", "JSMR.Api") // .Enrich.WithProperty("Service", "JSMR.Api")
.Enrich.WithProperty("Environment", env.EnvironmentName) // .Enrich.WithProperty("Environment", env.EnvironmentName)
.CreateLogger(); // .CreateLogger();
return services; // return services;
} //}
} }

View File

@@ -15,12 +15,18 @@ public static class WebApplicationExtensions
{ {
public static WebApplication UseAppPipeline(this WebApplication app, IHostEnvironment env) public static WebApplication UseAppPipeline(this WebApplication app, IHostEnvironment env)
{ {
app.UseCors("ui"); string[] origins = app.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() ?? [];
if (origins.Length > 0)
app.UseCors("ui");
if (env.IsDevelopment()) if (env.IsDevelopment())
app.MapOpenApi(); app.MapOpenApi();
app.UseHttpsRedirection(); if (!env.IsDevelopment())
{
app.UseHttpsRedirection();
}
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();

View File

@@ -25,11 +25,10 @@
}, },
"Enrich": [ "FromLogContext", "WithMachineName", "WithProcessId", "WithThreadId" ], "Enrich": [ "FromLogContext", "WithMachineName", "WithProcessId", "WithThreadId" ],
"WriteTo": [ "WriteTo": [
{ "Name": "Console" }, { "Name": "Console" }
{
"Name": "Seq",
"Args": { "serverUrl": "%SEQ_URL%" }
}
] ]
},
"Seq": {
"ServerUrl": ""
} }
} }

View File

@@ -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

View File

@@ -10,9 +10,14 @@ var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app"); builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after"); builder.RootComponents.Add<HeadOutlet>("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 // Old way
//builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(apiBase) }); //builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(apiBase) });

32
JSMR.UI.Blazor/nginx.conf Normal file
View File

@@ -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;
}
}

View File

@@ -0,0 +1,3 @@
{
"ApiBaseUrl": "https://localhost:7277"
}

View File

@@ -1,3 +1,3 @@
{ {
"ApiBaseUrl": "https://localhost:7277" "ApiBaseUrl": ""
} }

View File

@@ -17,6 +17,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JSMR.UI.Blazor", "JSMR.UI.B
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JSMR.Worker", "JSMR.Worker\JSMR.Worker.csproj", "{964BD375-FAE3-4044-A09B-5C43919C9B52}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JSMR.Worker", "JSMR.Worker\JSMR.Worker.csproj", "{964BD375-FAE3-4044-A09B-5C43919C9B52}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU

27
docker-compose.yml Normal file
View File

@@ -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: {}