feat/Update_Controllers_to_Allow_for_Premium_Membership
This commit is contained in:
+69
-31
@@ -24,6 +24,7 @@ using JobTrackerApi.Services.JobImport.Plugins;
|
||||
using JobTrackerApi.Services.JobImport.Translation;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var externalOrigin = ExternalOrigin.FromConfiguration(builder.Configuration, builder.Environment.IsProduction());
|
||||
|
||||
// Avoid Windows EventLog provider issues in local dev environments.
|
||||
builder.Logging.ClearProviders();
|
||||
@@ -35,7 +36,17 @@ else
|
||||
}
|
||||
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddScoped<ICurrentUserService, CurrentUserService>();
|
||||
builder.Services.AddSingleton(externalOrigin);
|
||||
builder.Services.AddSingleton<AiPrivacyPolicy>();
|
||||
builder.Services.AddTransient<AiPrivacyHeaderHandler>();
|
||||
builder.Services.AddScoped<CurrentUserService>();
|
||||
builder.Services.AddScoped<ICurrentUserService>(sp => sp.GetRequiredService<CurrentUserService>());
|
||||
builder.Services.AddSingleton<BackgroundTenantRunner>();
|
||||
builder.Services.AddSingleton(TimeProvider.System);
|
||||
builder.Services.AddScoped<UserOperationStore>();
|
||||
builder.Services.AddScoped<AiOperationAdmission>();
|
||||
builder.Services.AddSingleton<AiOperationWorker>();
|
||||
builder.Services.AddScoped<UserNotificationStore>();
|
||||
builder.Services.AddScoped<IEmailSettingsResolver, EmailSettingsResolver>();
|
||||
builder.Services.AddScoped<IAppEmailSender, SmtpEmailSender>();
|
||||
builder.Services.AddSingleton<ICvProcessingQueue, CvProcessingQueue>();
|
||||
@@ -55,6 +66,7 @@ builder.Services.AddScoped<IApplicationAssetsService, ApplicationAssetsService>(
|
||||
builder.Services.AddScoped<IInterviewPrepService, InterviewPrepService>();
|
||||
|
||||
builder.Services.AddSingleton<AppPaths>();
|
||||
builder.Services.AddSingleton<IAttachmentStorage, AttachmentStorage>();
|
||||
builder.Services.AddSingleton<IStartupReadiness, StartupReadiness>();
|
||||
|
||||
// Add DbContext
|
||||
@@ -151,6 +163,7 @@ builder.Services.AddHostedService<DailyExportHostedService>();
|
||||
builder.Services.AddHostedService<JobEnrichmentHostedService>();
|
||||
builder.Services.AddHostedService<SummarizerProbeHostedService>();
|
||||
builder.Services.AddHostedService<CvProcessingHostedService>();
|
||||
builder.Services.AddHostedService<AiOperationHostedService>();
|
||||
|
||||
builder.Services.AddHttpClient("jobimport")
|
||||
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
|
||||
@@ -174,7 +187,7 @@ builder.Services.AddHttpClient("ai-service", client =>
|
||||
{
|
||||
client.DefaultRequestHeaders.Add("X-Ai-Service-Token", serviceToken);
|
||||
}
|
||||
});
|
||||
}).AddHttpMessageHandler<AiPrivacyHeaderHandler>();
|
||||
|
||||
builder.Services.AddMemoryCache();
|
||||
builder.Services.AddScoped<AnalyticsService>();
|
||||
@@ -210,6 +223,7 @@ builder.Services.AddIdentityCore<ApplicationUser>(options =>
|
||||
})
|
||||
.AddRoles<IdentityRole>()
|
||||
.AddEntityFrameworkStores<JobTrackerContext>()
|
||||
.AddDefaultTokenProviders()
|
||||
.AddSignInManager();
|
||||
|
||||
builder.Services.AddScoped<ITokenService, TokenService>();
|
||||
@@ -236,6 +250,11 @@ builder.Services.AddScoped<JobImportService>();
|
||||
var requireAuth = builder.Configuration.GetValue("Auth:Require", false);
|
||||
var googleClientId = (builder.Configuration["Auth:GoogleClientId"] ?? "").Trim();
|
||||
var microsoftClientId = (builder.Configuration["Auth:MicrosoftClientId"] ?? "").Trim();
|
||||
var microsoftTenantWasDefaulted = !string.IsNullOrWhiteSpace(microsoftClientId)
|
||||
&& string.IsNullOrWhiteSpace(builder.Configuration["Auth:MicrosoftTenant"])
|
||||
&& !builder.Environment.IsProduction();
|
||||
if (!string.IsNullOrWhiteSpace(microsoftClientId))
|
||||
MicrosoftTenantPolicy.Parse(builder.Configuration["Auth:MicrosoftTenant"], builder.Environment.IsProduction());
|
||||
|
||||
var jwtKey = (builder.Configuration["Auth:JwtKey"] ?? "").Trim();
|
||||
var ephemeralJwtKey = false;
|
||||
@@ -261,7 +280,7 @@ builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
options.ForwardDefaultSelector = ctx =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(googleClientId) && string.IsNullOrWhiteSpace(microsoftClientId))
|
||||
if (string.IsNullOrWhiteSpace(googleClientId))
|
||||
return "local";
|
||||
|
||||
var auth = ctx.Request.Headers.Authorization.ToString();
|
||||
@@ -279,8 +298,6 @@ builder.Services.AddAuthentication(options =>
|
||||
var iss = jwt.Issuer ?? "";
|
||||
if (!string.IsNullOrWhiteSpace(googleClientId) && iss is "accounts.google.com" or "https://accounts.google.com")
|
||||
return "google";
|
||||
if (!string.IsNullOrWhiteSpace(microsoftClientId) && iss.StartsWith("https://login.microsoftonline.com/", StringComparison.OrdinalIgnoreCase))
|
||||
return "microsoft";
|
||||
return "local";
|
||||
}
|
||||
catch
|
||||
@@ -326,7 +343,11 @@ builder.Services.AddAuthentication(options =>
|
||||
// acceptable, same additive-forward cost the 2FA/trusted-device features on this
|
||||
// branch already paid) or forged, and either way isn't proof of a live session.
|
||||
var db = context.HttpContext.RequestServices.GetRequiredService<JobTrackerContext>();
|
||||
if (!await LocalSessionValidator.IsValidAsync(db, context.Principal, DateTimeOffset.UtcNow))
|
||||
if (!await LocalSessionValidator.IsValidAsync(
|
||||
db,
|
||||
context.Principal,
|
||||
DateTimeOffset.UtcNow,
|
||||
builder.Configuration.GetValue("Auth:RequireEmailVerification", false)))
|
||||
{
|
||||
context.Fail("Session has been revoked or expired.");
|
||||
}
|
||||
@@ -364,25 +385,10 @@ if (!string.IsNullOrWhiteSpace(googleClientId))
|
||||
});
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(microsoftClientId))
|
||||
{
|
||||
builder.Services.AddAuthentication().AddJwtBearer("microsoft", options =>
|
||||
{
|
||||
// Validate Microsoft (Entra ID / personal account) ID tokens as bearer tokens.
|
||||
// "common" authority + ValidateIssuer=false: multi-tenant issuer varies per tenant id.
|
||||
options.Authority = "https://login.microsoftonline.com/common/v2.0";
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateIssuer = false,
|
||||
ValidateAudience = true,
|
||||
ValidAudience = microsoftClientId,
|
||||
ValidateLifetime = true,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
builder.Services.AddAuthorization(options =>
|
||||
{
|
||||
options.AddPolicy(ProEntitlement.Policy, policy =>
|
||||
policy.AddRequirements(new ProEntitlementRequirement()));
|
||||
if (requireAuth)
|
||||
{
|
||||
options.FallbackPolicy = new AuthorizationPolicyBuilder()
|
||||
@@ -390,6 +396,8 @@ builder.Services.AddAuthorization(options =>
|
||||
.Build();
|
||||
}
|
||||
});
|
||||
builder.Services.AddScoped<IAuthorizationHandler, ProEntitlementHandler>();
|
||||
builder.Services.AddSingleton<IAuthorizationMiddlewareResultHandler, ProEntitlementAuthorizationResultHandler>();
|
||||
|
||||
builder.Services.AddRateLimiter(options =>
|
||||
{
|
||||
@@ -453,15 +461,26 @@ var enableHttpsRedirect = app.Configuration.GetValue("HttpsRedirection:Enabled",
|
||||
var enableHsts = app.Configuration.GetValue("HttpsRedirection:Hsts", false);
|
||||
if (app.Configuration.GetValue("Proxy:TrustForwardedHeaders", false))
|
||||
{
|
||||
var forwarded = new ForwardedHeadersOptions
|
||||
app.UseForwardedHeaders(ForwardedProxyConfiguration.Build(app.Configuration));
|
||||
}
|
||||
if (microsoftTenantWasDefaulted)
|
||||
{
|
||||
app.Logger.LogWarning("Auth:MicrosoftTenant was not configured; Development/Test defaults to common. Production requires an explicit value.");
|
||||
}
|
||||
|
||||
if (app.Environment.IsProduction())
|
||||
{
|
||||
app.Use(async (ctx, next) =>
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
|
||||
ForwardLimit = 1,
|
||||
};
|
||||
// This mode is enabled only when compose keeps the backend internal and nginx is the sole ingress.
|
||||
forwarded.KnownNetworks.Clear();
|
||||
forwarded.KnownProxies.Clear();
|
||||
app.UseForwardedHeaders(forwarded);
|
||||
if (!externalOrigin.AllowsRequest(ctx.Request.Host, ctx.Request.Path))
|
||||
{
|
||||
ctx.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||
await ctx.Response.WriteAsync("Unknown host.");
|
||||
return;
|
||||
}
|
||||
|
||||
await next();
|
||||
});
|
||||
}
|
||||
if (enableHsts) app.UseHsts();
|
||||
if (enableHttpsRedirect) app.UseHttpsRedirection();
|
||||
@@ -505,6 +524,24 @@ app.Use(async (ctx, next) =>
|
||||
|
||||
await app.InitializeJobTrackerAsync();
|
||||
|
||||
await using (var attachmentScope = app.Services.CreateAsyncScope())
|
||||
{
|
||||
var result = await attachmentScope.ServiceProvider.GetRequiredService<IAttachmentStorage>()
|
||||
.ReconcileAsync(attachmentScope.ServiceProvider.GetRequiredService<JobTrackerContext>(), CancellationToken.None);
|
||||
if (result.Missing > 0 || result.UnknownOrphans > 0 || result.UnsafePaths > 0 || result.Failures > 0)
|
||||
{
|
||||
app.Logger.LogWarning(
|
||||
"Attachment reconciliation completed with missing={Missing}, unknownOrphans={UnknownOrphans}, unsafePaths={UnsafePaths}, failures={Failures}; unknown files were not removed.",
|
||||
result.Missing, result.UnknownOrphans, result.UnsafePaths, result.Failures);
|
||||
}
|
||||
else if (result.Promoted > 0 || result.Restored > 0 || result.Purged > 0)
|
||||
{
|
||||
app.Logger.LogInformation(
|
||||
"Attachment reconciliation recovered promoted={Promoted}, restored={Restored}, purged={Purged} files.",
|
||||
result.Promoted, result.Restored, result.Purged);
|
||||
}
|
||||
}
|
||||
|
||||
app.UseCors("AllowReact");
|
||||
app.UseRateLimiter();
|
||||
|
||||
@@ -524,6 +561,7 @@ app.Use(async (ctx, next) =>
|
||||
|
||||
if (ctx.Request.Path.StartsWithSegments("/api/auth/login")
|
||||
|| ctx.Request.Path.StartsWithSegments("/api/auth/register")
|
||||
|| ctx.Request.Path.StartsWithSegments("/api/auth/logout")
|
||||
|| ctx.Request.Path.StartsWithSegments("/api/auth/google/exchange")
|
||||
|| ctx.Request.Path.StartsWithSegments("/api/auth/request-password-reset")
|
||||
|| ctx.Request.Path.StartsWithSegments("/api/auth/reset-password")
|
||||
|
||||
Reference in New Issue
Block a user