using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using Scalar.AspNetCore; using Serilog; using Serilog.Events; using System.Text; using System.Text.Json.Serialization; using Tiku.Api.Logging; using Tiku.Api.Middleware; using Tiku.Api.OpenApi; using Tiku.Api.Options; using Tiku.Api.Security; using Tiku.Application; using Tiku.Application.Security; using Tiku.Infrastructure; using Tiku.Infrastructure.Persistence; Log.Logger = new LoggerConfiguration() .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) .Enrich.FromLogContext() .WriteTo.Console() .CreateBootstrapLogger(); try { Log.Information("Starting TIKU API"); var builder = WebApplication.CreateBuilder(args); builder.Services.AddSerilog((services, configuration) => configuration .ReadFrom.Configuration(builder.Configuration) .ReadFrom.Services(services) .Enrich.FromLogContext(), preserveStaticLogger: true); builder.Services.AddControllers() .AddJsonOptions(options => { options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); }); builder.Services.AddOpenApi(options => { options.AddDocumentTransformer(); }); builder.Services.AddProblemDetails(); builder.Services.AddApplication(); builder.Services.Configure(builder.Configuration.GetSection(CorsOptions.SectionName)); var corsOptions = builder.Configuration .GetSection(CorsOptions.SectionName) .Get() ?? new CorsOptions(); builder.Services.AddCors(options => { options.AddPolicy(CorsOptions.PolicyName, policy => { var origins = corsOptions.AllowedOrigins .Where(origin => !string.IsNullOrWhiteSpace(origin)) .Select(origin => origin.Trim().TrimEnd('/')) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); if (origins.Length > 0) { policy.WithOrigins(origins); } policy .WithHeaders(corsOptions.AllowedHeaders) .WithMethods(corsOptions.AllowedMethods); if (corsOptions.AllowCredentials) { policy.AllowCredentials(); } }); }); var connectionString = builder.Configuration.GetConnectionString("Database") ?? builder.Configuration["DATABASE_URL"] ?? "Host=localhost;Database=tiku;Username=postgres"; builder.Services.AddInfrastructure(connectionString); builder.Services.Configure(builder.Configuration.GetSection("Security:Jwt")); var jwtOptions = builder.Configuration .GetSection("Security:Jwt") .Get() ?? new JwtOptions(); builder.Services .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = jwtOptions.Issuer, ValidateAudience = true, ValidAudience = jwtOptions.Audience, ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.SigningKey)), ValidateLifetime = true, ClockSkew = TimeSpan.FromMinutes(1) }; options.Events = new JwtBearerEvents { OnTokenValidated = async context => { if (!jwtOptions.ValidateSessions) { return; } var sessionIdValue = context.Principal?.FindFirst(TikuClaimTypes.SessionId)?.Value; if (!Guid.TryParse(sessionIdValue, out var sessionId)) { context.Fail("Missing session claim."); return; } var dbContext = context.HttpContext.RequestServices.GetRequiredService(); var now = DateTimeOffset.UtcNow; var isSessionActive = await dbContext.AuthSessions.AnyAsync( session => session.Id == sessionId && session.RevokedAt == null && session.ExpiresAt > now); if (!isSessionActive) { context.Fail("Session has been revoked or expired."); } } }; }); builder.Services.AddAuthorization(options => { options.AddPolicy( TikuPolicies.AuthenticatedUser, policy => policy.RequireAuthenticatedUser()); options.AddPolicy( TikuPolicies.CurrentTenantMember, policy => policy .RequireAuthenticatedUser() .RequireAssertion(context => TenantRoleAuthorization.IsTenantMember(context.User))); options.AddPolicy( TikuPolicies.TenantAdmin, policy => policy .RequireAuthenticatedUser() .RequireAssertion(context => TenantRoleAuthorization.IsTenantAdmin(context.User))); }); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.MapOpenApi(); app.MapScalarApiReference(options => options .WithTitle("TIKU Backend API") .AddPreferredSecuritySchemes("BearerAuth") .EnablePersistentAuthentication()); } app.UseSerilogRequestLogging(SerilogRequestLogging.ConfigureRequestLogging); app.UseMiddleware(); app.UseHttpsRedirection(); app.UseCors(CorsOptions.PolicyName); app.UseAuthentication(); app.UseMiddleware(); app.UseAuthorization(); app.MapControllers(); app.Run(); } catch (Exception exception) { Log.Fatal(exception, "TIKU API terminated unexpectedly"); throw; } finally { Log.CloseAndFlush(); } public partial class Program;