NALU/src/Nalu.Api/Program.cs
Ricardo Carneiro ea6cdb5395 Initial commit — NALU AI web platform
- ASP.NET Core 9 Razor Pages + Minimal API hybrid
- 14 validators (CPF, CEP, CNPJ, email, phone, name, yes-no, birthdate, handoff, cancel-intent, company-name, plate-br, postal-code, validate_reply)
- OAuth login (Google, Microsoft, GitHub) + cookie auth
- MongoDB usage tracking + CEP cache collection
- Stripe checkout with inline PriceData (no Price IDs)
- MCP server for Claude Code / Cursor integration
- Playground (10 calls/IP/day, no auth)
- Docs: Quickstart, API Reference, N8N, MCP, Créditos, Erros, Fluxos
- Credit system: 3 cr standard validators, 5 cr validate_reply
- SmartSuggestion: contextual re-ask via IA when obtained=false
- Per-IP rate limiting + daily cap + shared-IP abuse detection
- Lightbox for comic images
- Validadores page split: Brasileiros / Universais + Em breve

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 16:39:04 -03:00

267 lines
13 KiB
C#

using AspNet.Security.OAuth.GitHub;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.Google;
using Microsoft.AspNetCore.Authentication.MicrosoftAccount;
using MongoDB.Driver;
using Nalu.Web.Data;
using Nalu.Web.Data.Repositories;
using Nalu.Web.Endpoints;
using Nalu.Web.Enrichers;
using Nalu.Web.Infrastructure;
using Nalu.Web.Mcp;
using Nalu.Web.PostProcessors;
using Nalu.Web.Services;
using Nalu.Web.Services.LlmRouter;
using Scalar.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
// ── Razor Pages (site) ───────────────────────────────────────────────────────
builder.Services.AddRazorPages();
// ── OpenAPI / Scalar ──────────────────────────────────────────────────────────
builder.Services.AddOpenApi("v1", options =>
{
options.AddDocumentTransformer((doc, _, _) =>
{
doc.Info.Title = "NALU AI API";
doc.Info.Version = "v1";
doc.Info.Description = "Natural Language Understanding — extrai intenções reais de diálogos agente/usuário.";
return Task.CompletedTask;
});
});
// ── MongoDB ───────────────────────────────────────────────────────────────────
builder.Services.AddSingleton<IMongoClient>(sp =>
{
var connStr = builder.Configuration.GetConnectionString("MongoDB") ?? string.Empty;
if (string.IsNullOrWhiteSpace(connStr)) return null!;
var settings = MongoClientSettings.FromConnectionString(
connStr + (connStr.Contains('?') ? "&" : "?") +
"maxPoolSize=200&minPoolSize=10&maxIdleTimeMS=30000");
return new MongoClient(settings);
});
builder.Services.AddSingleton<MongoDbContext>();
// ── Repositories ──────────────────────────────────────────────────────────────
builder.Services.AddSingleton<ApiKeyRepository>();
builder.Services.AddSingleton<UserRepository>();
builder.Services.AddSingleton<UsageRepository>();
builder.Services.AddSingleton<SubscriptionRepository>();
builder.Services.AddSingleton<WebhookEventRepository>();
// ── Authentication ───────────────────────────────────────────────────────────
// Site = Cookie (Razor Pages). API = Bearer API key (/v1/*). DO NOT mix.
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
options.LoginPath = "/login";
options.LogoutPath = "/auth/logout";
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.Lax;
options.ExpireTimeSpan = TimeSpan.FromDays(30);
options.SlidingExpiration = true;
})
// Temporary cookie for external OAuth handshake
.AddCookie("ExternalCookie", options =>
{
options.Cookie.Name = "nalu.external";
options.ExpireTimeSpan = TimeSpan.FromMinutes(10);
})
// API key scheme — used explicitly by /v1/* endpoints
.AddScheme<AuthenticationSchemeOptions, NaluAuthHandler>(ApiKeyAuthScheme.Name, null)
.AddGoogle(GoogleDefaults.AuthenticationScheme, options =>
{
options.ClientId = builder.Configuration["OAuth:Google:ClientId"] ?? string.Empty;
options.ClientSecret = builder.Configuration["OAuth:Google:ClientSecret"] ?? string.Empty;
options.CallbackPath = "/signin-google";
options.SignInScheme = "ExternalCookie";
options.SaveTokens = false;
})
.AddMicrosoftAccount(MicrosoftAccountDefaults.AuthenticationScheme, options =>
{
options.ClientId = builder.Configuration["OAuth:Microsoft:ClientId"] ?? string.Empty;
options.ClientSecret = builder.Configuration["OAuth:Microsoft:ClientSecret"] ?? string.Empty;
options.CallbackPath = "/signin-microsoft";
options.SignInScheme = "ExternalCookie";
options.SaveTokens = false;
})
.AddGitHub(GitHubAuthenticationDefaults.AuthenticationScheme, options =>
{
options.ClientId = builder.Configuration["OAuth:GitHub:ClientId"] ?? string.Empty;
options.ClientSecret = builder.Configuration["OAuth:GitHub:ClientSecret"] ?? string.Empty;
options.CallbackPath = "/signin-github";
options.SignInScheme = "ExternalCookie";
options.Scope.Add("user:email");
options.SaveTokens = false;
});
builder.Services.AddAuthorization(options =>
{
// API endpoints — must present a valid Bearer API key
options.AddPolicy("ApiKey", policy =>
policy.AddAuthenticationSchemes(ApiKeyAuthScheme.Name)
.RequireAuthenticatedUser());
});
// ── Session (required for OAuth state) ───────────────────────────────────────
builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession(options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
options.IdleTimeout = TimeSpan.FromMinutes(10);
});
// ── Caching ──────────────────────────────────────────────────────────────────
builder.Services.AddMemoryCache();
// ── HTTP clients ─────────────────────────────────────────────────────────────
builder.Services.AddHttpClient<GroqClient>(client =>
{
var baseUrl = builder.Configuration["Groq:BaseUrl"] ?? "https://api.groq.com/openai/v1";
client.BaseAddress = new Uri(baseUrl.TrimEnd('/') + '/');
client.DefaultRequestHeaders.Add(
"Authorization",
$"Bearer {builder.Configuration["Groq:ApiKey"]}");
client.Timeout = TimeSpan.FromSeconds(30);
});
// ── LLM Router (Groq → OpenRouter → Google AI) ───────────────────────────────
builder.Services.AddHttpClient<GroqProvider>(client =>
{
var baseUrl = builder.Configuration["Groq:BaseUrl"] ?? "https://api.groq.com/openai/v1";
client.BaseAddress = new Uri(baseUrl.TrimEnd('/') + '/');
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {builder.Configuration["Groq:ApiKey"]}");
client.Timeout = TimeSpan.FromSeconds(30);
});
builder.Services.AddHttpClient<OpenRouterProvider>(client =>
{
var baseUrl = builder.Configuration["OpenRouter:BaseUrl"] ?? "https://openrouter.ai/api/v1";
client.BaseAddress = new Uri(baseUrl.TrimEnd('/') + '/');
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {builder.Configuration["OpenRouter:ApiKey"]}");
client.DefaultRequestHeaders.Add("HTTP-Referer", "https://naluai.com");
client.DefaultRequestHeaders.Add("X-Title", "NALU AI");
client.Timeout = TimeSpan.FromSeconds(30);
});
builder.Services.AddHttpClient<GoogleAiProvider>(client =>
{
var baseUrl = builder.Configuration["GoogleAi:BaseUrl"] ?? "https://generativelanguage.googleapis.com/v1beta/openai/";
client.BaseAddress = new Uri(baseUrl.TrimEnd('/') + '/');
client.DefaultRequestHeaders.Add("Authorization", $"Bearer {builder.Configuration["GoogleAi:ApiKey"]}");
client.Timeout = TimeSpan.FromSeconds(30);
});
builder.Services.AddSingleton<ILlmRouter>(sp =>
{
var providers = new List<ILlmProvider>
{
sp.GetRequiredService<GroqProvider>(),
sp.GetRequiredService<OpenRouterProvider>(),
sp.GetRequiredService<GoogleAiProvider>()
};
return new LlmRouter(providers, sp.GetRequiredService<ILogger<LlmRouter>>());
});
// ViaCEP enricher — no fixed base address (calls multiple URLs)
builder.Services.AddHttpClient<ViaCepEnricher>(client =>
{
client.Timeout = TimeSpan.FromSeconds(10);
client.DefaultRequestHeaders.Add("User-Agent", "nalu-ai/1.0");
});
builder.Services.AddTransient<IEnricher>(sp => sp.GetRequiredService<ViaCepEnricher>());
// ── Post-processors ───────────────────────────────────────────────────────────
builder.Services.AddSingleton<IPostProcessor, CapitalizeProperName>();
builder.Services.AddSingleton<IPostProcessor, RemoveTitles>();
builder.Services.AddSingleton<IPostProcessor, ValidateCpfDigit>();
builder.Services.AddSingleton<IPostProcessor, FormatPhone>();
builder.Services.AddSingleton<IPostProcessor, FormatCep>();
builder.Services.AddSingleton<IPostProcessor, CorrectEmailTypos>();
builder.Services.AddSingleton<IPostProcessor, NormalizePostalCode>();
builder.Services.AddSingleton<IPostProcessor, ParseDate>();
builder.Services.AddSingleton<IPostProcessor, CalculateAge>();
builder.Services.AddSingleton<IPostProcessor, ValidateCnpjDigit>();
builder.Services.AddSingleton<IPostProcessor, FormatPlate>();
builder.Services.AddSingleton<IPostProcessor, SelectHandoffSuggestion>();
builder.Services.AddSingleton<IPostProcessor, SelectCancelSuggestion>();
// ── Domain services ──────────────────────────────────────────────────────────
builder.Services.AddScoped<UserService>();
builder.Services.AddSingleton<ValidatorLoader>();
builder.Services.AddSingleton<DeterministicLayer>();
builder.Services.AddSingleton<PostProcessorRegistry>();
builder.Services.AddSingleton<SuggestionBuilder>();
builder.Services.AddSingleton<CacheService>();
builder.Services.AddSingleton<AuthService>();
builder.Services.AddSingleton<RateLimitService>();
builder.Services.AddSingleton<CreditService>();
// EnrichmentService is Scoped because ViaCepEnricher is Transient (HttpClient lifecycle)
builder.Services.AddScoped<EnrichmentService>();
builder.Services.AddScoped<LlmExtractionService>();
builder.Services.AddScoped<ExtractionPipeline>();
builder.Services.AddScoped<ReplyService>();
// ── MCP server ────────────────────────────────────────────────────────────────
builder.Services.AddSingleton<McpServer>();
// ── App ───────────────────────────────────────────────────────────────────────
var app = builder.Build();
// Initialize MongoDB indexes on startup
var mongo = app.Services.GetRequiredService<MongoDbContext>();
await mongo.InitializeAsync();
app.UseSession();
app.UseAuthentication();
app.UseAuthorization();
app.MapOpenApi();
app.MapScalarApiReference(options =>
{
options.Title = "NALU AI";
options.DefaultHttpClient = new(ScalarTarget.Http, ScalarClient.HttpClient);
});
app.MapRazorPages();
app.MapExtractEndpoints();
app.MapValidatorsEndpoints();
// ── Auth endpoints ────────────────────────────────────────────────────────────
app.MapGet("/auth/logout", async (HttpContext ctx) =>
{
await ctx.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Results.Redirect("/");
}).AllowAnonymous();
// ── Health check ──────────────────────────────────────────────────────────────
app.MapGet("/health", () => Results.Ok(new { status = "ok", ts = DateTime.UtcNow }))
.WithTags("Health")
.WithSummary("Health check")
.AllowAnonymous();
// ── MCP endpoint ──────────────────────────────────────────────────────────────
app.MapPost("/mcp", async (HttpContext ctx, McpServer mcp, CancellationToken ct) =>
await mcp.HandleAsync(ctx, ct))
.RequireAuthorization("ApiKey")
.WithName("McpEndpoint")
.WithSummary("MCP Server (JSON-RPC 2.0 / Streamable HTTP)")
.WithTags("MCP")
.WithOpenApi();
app.Run();
// Exposed for WebApplicationFactory in integration tests
public partial class Program { }