feat/Update_Controllers_to_Allow_for_Premium_Membership

This commit is contained in:
cesnimda
2026-08-03 09:17:28 +02:00
parent de937d25dc
commit c3f4a57195
187 changed files with 26062 additions and 991 deletions
+69 -31
View File
@@ -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")