using System.Text; using FlatRender.IdentitySvc.Application.Services; using FlatRender.IdentitySvc.Application.Services.Interfaces; using FlatRender.IdentitySvc.Domain.Enums; using FlatRender.IdentitySvc.Infrastructure.Data; using FlatRender.IdentitySvc.Middleware; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; using Npgsql; var builder = WebApplication.CreateBuilder(args); // ── Database ────────────────────────────────────────────────────────────── var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("ConnectionStrings:DefaultConnection is required"); // Native PostgreSQL enums are mapped on the EF provider so Npgsql can read/write them // at runtime (HasPostgresEnum in the model alone is not enough on Npgsql 8+). EF builds // the data source with these mappings. PG labels are PascalCase and match the C# enum // member names, so a preserve-case name translator is used for the values. var enumTr = PreserveCaseNameTranslator.Instance; builder.Services.AddDbContext(options => options.UseNpgsql( connectionString, npgsql => { npgsql.MigrationsHistoryTable("__ef_migrations", "identity"); npgsql.MapEnum("tenant_status", "identity", enumTr); npgsql.MapEnum("tenant_kind", "identity", enumTr); npgsql.MapEnum("register_mode", "identity", enumTr); npgsql.MapEnum("gender_kind", "identity", enumTr); npgsql.MapEnum("token_purpose", "identity", enumTr); npgsql.MapEnum("mfa_factor_type", "identity", enumTr); npgsql.MapEnum("plan_scope", "identity", enumTr); npgsql.MapEnum("billing_period", "identity", enumTr); npgsql.MapEnum("payment_gateway", "identity", enumTr); npgsql.MapEnum("payment_status", "identity", enumTr); npgsql.MapEnum("payment_action", "identity", enumTr); npgsql.MapEnum("discount_kind", "identity", enumTr); npgsql.MapEnum("quest_type", "identity", enumTr); npgsql.MapEnum("prize_type", "identity", enumTr); npgsql.MapEnum("gift_type", "identity", enumTr); } ) .UseSnakeCaseNamingConvention() ); // ── JWT Auth ────────────────────────────────────────────────────────────── var jwtSecret = builder.Configuration["Jwt:Secret"] ?? throw new InvalidOperationException("Jwt:Secret is required"); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSecret)), ValidateIssuer = true, ValidIssuer = builder.Configuration["Jwt:Issuer"] ?? "flatrender-identity", ValidateAudience = true, ValidAudience = builder.Configuration["Jwt:Audience"] ?? "flatrender", ValidateLifetime = true, ClockSkew = TimeSpan.Zero, }; }); builder.Services.AddAuthorization(); // ── Services ────────────────────────────────────────────────────────────── builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // ── HTTP clients ─────────────────────────────────────────────────────────── builder.Services.AddHttpClient("zarinpal", client => { client.DefaultRequestHeaders.Accept.Add( new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json")); client.Timeout = TimeSpan.FromSeconds(10); }); builder.Services.AddHttpClient("snappay", client => { client.DefaultRequestHeaders.Accept.Add( new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json")); client.Timeout = TimeSpan.FromSeconds(15); // SnapPay token exchange can be slow }); builder.Services.AddHttpClient("tara", client => { client.DefaultRequestHeaders.Accept.Add( new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json")); client.Timeout = TimeSpan.FromSeconds(10); }); // ── Routing ─────────────────────────────────────────────────────────────── builder.Services.AddRouting(opts => { opts.LowercaseUrls = true; opts.AppendTrailingSlash = false; }); // ── Controllers + Swagger ───────────────────────────────────────────────── builder.Services.AddControllers() .AddJsonOptions(o => o.JsonSerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.SnakeCaseLower); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "FlatRender Identity Service", Version = "v1" }); c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { Description = "JWT Bearer token. Format: Bearer {token}", Name = "Authorization", In = ParameterLocation.Header, Type = SecuritySchemeType.ApiKey, Scheme = "Bearer", }); c.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } }, Array.Empty() } }); }); builder.Services.AddCors(options => options.AddDefaultPolicy(policy => policy.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader())); builder.Services.AddHealthChecks() .AddCheck("db", () => { // actual DB ping happens at startup migration; just return healthy here return Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult.Healthy(); }); var app = builder.Build(); app.UseMiddleware(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseCors(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.MapHealthChecks("/health"); if (app.Environment.IsDevelopment()) { using var scope = app.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); db.Database.Migrate(); } app.Run();