proposal-system/api/src/ProposalSystem.Api/Program.cs
Adam Moussa f44caba906 Fix JSON enum serialization and audit log jsonb format
Add JsonStringEnumConverter so string enum values (ServiceCategory,
Priority, PricingMode, LineItemSource) deserialize correctly from
frontend requests. Wrap AuditService detail strings in a JSON object
to satisfy the Postgres jsonb column type. Relax global.json SDK
version to match installed 8.0.1xx feature band.
2026-05-20 17:23:55 -04:00

187 lines
6 KiB
C#

using System.Text;
using Amazon.S3;
using Amazon.SecretsManager;
using Amazon.SQS;
using FluentValidation;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using ProposalSystem.Api.Middleware;
using ProposalSystem.Api.Services;
using ProposalSystem.Application.Interfaces;
using ProposalSystem.Application.Validators;
using ProposalSystem.Infrastructure.Data;
using ProposalSystem.Infrastructure.Services;
var builder = WebApplication.CreateBuilder(args);
// Dev mode flag (read early for conditional setup)
var devMode = builder.Configuration.GetValue<bool>("Auth:DevMode");
// AWS SDK clients (skip in dev mode — no real AWS credentials needed)
if (!devMode)
{
builder.Services.AddDefaultAWSOptions(builder.Configuration.GetAWSOptions());
builder.Services.AddAWSService<IAmazonS3>();
builder.Services.AddAWSService<IAmazonSQS>();
builder.Services.AddAWSService<IAmazonSecretsManager>();
}
// Database
var dbSecretArn = builder.Configuration["DB_SECRET_ARN"];
if (!string.IsNullOrEmpty(dbSecretArn))
{
var smClient = new AmazonSecretsManagerClient();
var connectionString = SecretsManagerConnectionString.ResolveAsync(smClient, dbSecretArn).GetAwaiter().GetResult();
builder.Services.AddDbContext<ProposalDbContext>(options =>
options.UseNpgsql(connectionString));
}
else
{
builder.Services.AddDbContext<ProposalDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
}
// Internal API key (for Lambda-to-API calls)
var internalApiKeySecretArn = builder.Configuration["INTERNAL_API_KEY_SECRET_ARN"];
if (!string.IsNullOrEmpty(internalApiKeySecretArn))
{
var smClient = new AmazonSecretsManagerClient();
var secretResponse = smClient.GetSecretValueAsync(new Amazon.SecretsManager.Model.GetSecretValueRequest
{
SecretId = internalApiKeySecretArn,
}).GetAwaiter().GetResult();
builder.Configuration["INTERNAL_API_KEY"] = secretResponse.SecretString;
}
// Authentication
var cognitoAuthority = builder.Configuration["Auth:Authority"];
if (!string.IsNullOrEmpty(cognitoAuthority))
{
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = cognitoAuthority;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidateAudience = false,
ValidateLifetime = true,
RoleClaimType = "cognito:groups",
};
});
}
else if (devMode)
{
var devSigningKey = builder.Configuration["Auth:DevSigningKey"]!;
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(devSigningKey)),
ValidateIssuer = true,
ValidIssuer = "proposal-system-dev",
ValidateAudience = true,
ValidAudience = "proposal-system-dev",
ValidateLifetime = true,
RoleClaimType = "cognito:groups",
};
});
}
else
{
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer();
}
builder.Services.AddAuthorization();
// Services
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ICurrentUserService, CurrentUserService>();
builder.Services.AddScoped<IProposalService, ProposalService>();
builder.Services.AddScoped<ILineItemService, LineItemService>();
builder.Services.AddScoped<ICustomerService, CustomerService>();
builder.Services.AddScoped<IAuditService, AuditService>();
builder.Services.AddScoped<IProposalNumberGenerator, ProposalNumberGenerator>();
if (devMode)
builder.Services.AddScoped<IS3Service, DevS3Service>();
else
builder.Services.AddScoped<IS3Service, S3Service>();
builder.Services.AddScoped<ISimilarProposalService, SimilarProposalService>();
var jobsQueueUrl = builder.Configuration["JOBS_QUEUE_URL"] ?? "";
if (string.IsNullOrEmpty(jobsQueueUrl))
{
builder.Services.AddScoped<IJobPublisher, NoOpJobPublisher>();
}
else
{
builder.Services.AddScoped<IJobPublisher>(sp =>
{
var sqsClient = sp.GetRequiredService<IAmazonSQS>();
return new SqsJobPublisher(sqsClient, jobsQueueUrl);
});
}
// HTTP client for Cognito token exchange
builder.Services.AddHttpClient();
// Validation
builder.Services.AddValidatorsFromAssemblyContaining<CreateProposalValidator>();
// Controllers
builder.Services.AddControllers(options =>
{
options.Filters.Add<ValidationFilter>();
}).AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter());
});
// Middleware
builder.Services.AddTransient<GlobalExceptionHandler>();
// Health checks
builder.Services.AddHealthChecks()
.AddDbContextCheck<ProposalDbContext>();
// CORS
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.WithOrigins(
"https://proposals.seahaven.com",
"http://localhost:5173")
.AllowAnyMethod()
.AllowAnyHeader();
});
});
// Lambda hosting
builder.Services.AddAWSLambdaHosting(LambdaEventSource.HttpApi);
var app = builder.Build();
app.UseMiddleware<GlobalExceptionHandler>();
app.UseCors();
app.UseMiddleware<InternalApiKeyMiddleware>();
app.UseAuthentication();
app.UseAuthorization();
app.Use(async (context, next) =>
{
if (context.User.Identity?.IsAuthenticated == true)
{
var userService = context.RequestServices.GetRequiredService<ICurrentUserService>();
await userService.ResolveAsync();
}
await next();
});
app.MapControllers();
app.MapHealthChecks("/api/health");
app.Run();