mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-10-07 15:08:58 +00:00
Addresses 29 FIX-severity findings and accessibility/code-quality NITs from the 2026-05-20 audit. Key changes: - Add confirmation dialogs for Mark as Sent and Create Revision (FIX-17) - Restrict S3 CORS from wildcard to specific origins (FIX-38) - Add API Gateway throttling at 100 rps / 50 burst (FIX-39) - Separate vendor upload from SQS extraction trigger (FIX-04/05) - Copy TotalBidAmount on proposal revision (FIX-11) - Add CI paths-ignore and concurrency group (FIX-47) - Add post-deploy health check (FIX-46) - Fix N+1 query, Guid.Empty FK, pagination bounds, role sync (FIX-01/02/07/09) - Fix dashboard OOM, status transitions, audit error handling (FIX-03/06/12) - Fix frontend date filters, error display, currency formatting (FIX-14/16/18-23) - Remove AOSS dashboard public access, skip empty AI suggestions (FIX-41/45) - Add aria-labels, document.title management, deduplicate constants - Restrict CORS localhost to development, log invalid API key attempts [skip deploy]
188 lines
6.1 KiB
C#
188 lines
6.1 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 =>
|
|
{
|
|
var origins = new List<string> { "https://proposals.seahaven.com" };
|
|
if (builder.Environment.IsDevelopment())
|
|
origins.Add("http://localhost:5173");
|
|
policy.WithOrigins(origins.ToArray())
|
|
.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();
|