using System.Security.Claims; using JobTrackerApi.Models; using Microsoft.AspNetCore.Identity; namespace JobTrackerApi.Services; public sealed record AiPrivacyDecision(bool ExternalProcessingAllowed, string Provider); public sealed class AiPrivacyPolicy(IConfiguration configuration, IServiceScopeFactory scopes) { public const string ExternalAllowedHeader = "X-Ai-External-Allowed"; public const string TaskTypeHeader = "X-Ai-Task-Type"; public string ExternalProvider { get { var provider = (configuration["Ai:ExternalProvider"] ?? "ollama").Trim().ToLowerInvariant(); return provider is "gemini" or "groq" ? provider : "ollama"; } } public string RoutingMode { get { var mode = (configuration["Ai:RoutingMode"] ?? "local_first").Trim().ToLowerInvariant(); return mode is "local_only" or "local_first" or "external_only" ? mode : "local_only"; } } public bool ExternalProcessingAvailable => configuration.GetValue("Ai:ExternalProcessingEnabled", false) && RoutingMode != "local_only" && ExternalProvider is "gemini" or "groq"; public async Task EvaluateAsync(string? userId, CancellationToken cancellationToken = default) { if (!ExternalProcessingAvailable || string.IsNullOrWhiteSpace(userId)) return new AiPrivacyDecision(false, "local"); await using var scope = scopes.CreateAsyncScope(); var users = scope.ServiceProvider.GetRequiredService>(); var user = await users.FindByIdAsync(userId); cancellationToken.ThrowIfCancellationRequested(); if (user is null || !user.AiEnabled || !user.ExternalAiProcessingAllowed) return new AiPrivacyDecision(false, "local"); var isPro = AccountPlans.ForRoles(await users.GetRolesAsync(user)).Ai; return isPro ? new AiPrivacyDecision(true, ExternalProvider) : new AiPrivacyDecision(false, "local"); } } public sealed class AiPrivacyHeaderHandler( IHttpContextAccessor httpContext, AiPrivacyPolicy privacyPolicy, AiOperationExecutionScope executionScope) : DelegatingHandler { protected override async Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { if (request.RequestUri?.AbsolutePath.StartsWith("/cv/", StringComparison.OrdinalIgnoreCase) == true) { var operationContext = executionScope.Current; var externalAllowed = operationContext?.EffectivePrivacyPolicy == "external_allowed"; if (operationContext is not null) request.Headers.TryAddWithoutValidation(AiPrivacyPolicy.TaskTypeHeader, operationContext.Lease.TaskType); else { var userId = httpContext.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier) ?? httpContext.HttpContext?.User.FindFirstValue("sub"); externalAllowed = (await privacyPolicy.EvaluateAsync(userId, cancellationToken)).ExternalProcessingAllowed; } if (externalAllowed) request.Headers.TryAddWithoutValidation(AiPrivacyPolicy.ExternalAllowedHeader, "true"); } return await base.SendAsync(request, cancellationToken); } }