account-tracking/src/AccountTracking.Api/Program.cs

117 lines
5.1 KiB
C#

using System.Text;
using AccountTracking.Api.Data;
using AccountTracking.Api.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Polly;
var builder = WebApplication.CreateBuilder(args);
// ── Configuration ──────────────────────────────────────────────────────────
var connectionString = builder.Configuration["DB_CONNECTION"];
var jwtSecret = builder.Configuration["JWT_SECRET"]
?? throw new InvalidOperationException("JWT_SECRET is required");
var appUsername = builder.Configuration["APP_USERNAME"]
?? throw new InvalidOperationException("APP_USERNAME is required");
var appPassword = builder.Configuration["APP_PASSWORD"]
?? throw new InvalidOperationException("APP_PASSWORD is required");
var allowedOrigin = builder.Configuration["ALLOWED_ORIGIN"] ?? "http://localhost:3000";
// ── Database ────────────────────────────────────────────────────────────────
if (!string.IsNullOrEmpty(connectionString))
{
builder.Services.AddDbContext<AppDbContext>(opts =>
opts.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)));
}
else
{
// NSwag build-time spec generation: no real DB available
builder.Services.AddDbContext<AppDbContext>(opts =>
opts.UseInMemoryDatabase("nswag_gen"));
}
// ── Services ────────────────────────────────────────────────────────────────
builder.Services.AddScoped<CsvImportService>();
builder.Services.AddScoped<DashboardService>();
// ── Auth ────────────────────────────────────────────────────────────────────
var keyBytes = Convert.FromBase64String(jwtSecret);
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opts =>
{
opts.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(keyBytes),
ValidateIssuer = false,
ValidateAudience = false,
ClockSkew = TimeSpan.Zero
};
});
builder.Services.AddAuthorization();
// Store credentials for use by AuthController
builder.Services.AddSingleton(new AppCredentials(appUsername, appPassword, keyBytes));
// ── CORS ────────────────────────────────────────────────────────────────────
builder.Services.AddCors(opts =>
{
opts.AddDefaultPolicy(policy =>
policy.WithOrigins(allowedOrigin, "http://localhost:3000")
.AllowAnyHeader()
.AllowAnyMethod());
});
// ── Controllers & OpenAPI ───────────────────────────────────────────────────
builder.Services.AddControllers();
builder.Services.AddOpenApiDocument(config =>
{
config.Title = "AccountTracking API";
config.Version = "v1";
config.AddSecurity("Bearer", new NSwag.OpenApiSecurityScheme
{
Type = NSwag.OpenApiSecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT"
});
});
// ── Kestrel: limit upload to 10 MB ─────────────────────────────────────────
builder.WebHost.ConfigureKestrel(opts =>
opts.Limits.MaxRequestBodySize = 10 * 1024 * 1024);
var app = builder.Build();
// ── Migrate DB (skip when running without a real DB) ───────────────────────
if (!string.IsNullOrEmpty(connectionString))
{
var retryPolicy = Policy
.Handle<Exception>()
.WaitAndRetry(
5,
attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt - 1)),
(ex, delay, attempt, _) =>
Console.WriteLine($"DB migration attempt {attempt} failed: {ex.Message}. Retrying in {delay.TotalSeconds}s..."));
using var scope = app.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
retryPolicy.Execute(() => db.Database.Migrate());
}
// ── Middleware pipeline ─────────────────────────────────────────────────────
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.UseOpenApi();
app.UseSwaggerUi();
app.MapControllers();
app.Run();
// Accessible by tests
public partial class Program { }
// Credential container registered in DI
public record AppCredentials(string Username, string PasswordHash, byte[] JwtKeyBytes);