using BoredCareers.Controllers.Payment; using BoredCareers.Services; using BoredCareers.Services.DatabaseService; using System.Threading.RateLimiting; using Stripe; using System.Security.Claims; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Cryptography; using BoredCareers.Services.TimerService; using BoredCareers.Entities; var builder = WebApplication.CreateBuilder(args); // Disable null warnings becuse string.IsNullOrEmpty checks for NULL or Empty #pragma warning disable CS8604 //////////////////////////////// /////// Database Service /////// //////////////////////////////// // Address string? _dbserver = Environment.GetEnvironmentVariable("MySQLServer"); string dbserver = !string.IsNullOrEmpty(_dbserver) ? _dbserver : "localhost"; // Database string? _dbdatabase = Environment.GetEnvironmentVariable("MySQLDatabase"); string dbdatabase = !string.IsNullOrEmpty(_dbdatabase) ? _dbdatabase : "boredcareers"; // UserName string? _dbuser = Environment.GetEnvironmentVariable("MySQLUser"); string dbUser = !string.IsNullOrEmpty(_dbuser) ? _dbuser : "root"; // Password string? _dbpass = Environment.GetEnvironmentVariable("MySQLPass"); string dbPass = !string.IsNullOrEmpty(_dbpass) ? _dbpass : "oasv34$8gpv023dd"; // Create the database serivice builder.Services.AddSingleton(sp => new DatabaseService("server=" + dbserver + ";user=" + dbUser + ";database=" + dbdatabase + ";password=" + dbPass + ";port=3306;OldGuids=true;") ); //////////////////////////////// ///////// Email Service //////// //////////////////////////////// // Address string? _eServer = Environment.GetEnvironmentVariable("EmailServer"); string EmailServer = !string.IsNullOrEmpty(_eServer) ? _eServer : "mail.mistox.com"; // Port string? _ePort = Environment.GetEnvironmentVariable("EmailPort"); int EmailPort = !string.IsNullOrEmpty(_ePort) ? Convert.ToInt32(_ePort) : 587; // User string? _eAddress = Environment.GetEnvironmentVariable("EmailAddress"); string EmailAddress = !string.IsNullOrEmpty(_eAddress) ? _eAddress : "no-reply@mistox.com"; // Password string? _ePassword = Environment.GetEnvironmentVariable("EmailPassword"); string EmailPassword = !string.IsNullOrEmpty(_ePassword) ? _ePassword : ""; // Create the email service EmailService Emailservice = new EmailService( EmailServer, EmailPort, EmailAddress, EmailPassword ); builder.Services.Add( new ServiceDescriptor( typeof( EmailService ), Emailservice )); //////////////////////////////// /////// Payment Service //////// //////////////////////////////// // Payment service name -> must be name of PaymentType enum string? PaymentService = Environment.GetEnvironmentVariable("PaymentService"); IPayment._PaymentType = (PaymentType)Enum.Parse(typeof(PaymentType), PaymentService, true); if (IPayment._PaymentType == PaymentType.StripeIntent) { // Get PublicKey string? StripePublicKey = Environment.GetEnvironmentVariable("StripePublicKey"); IPayment._PublicKey = string.IsNullOrEmpty(StripePublicKey) ? "" : StripePublicKey; // Get PrivateKey string? StripeAPIKey = Environment.GetEnvironmentVariable("StripeApiKey"); StripeConfiguration.ApiKey = StripeAPIKey; // Get Endpoint secret string? StripeEndpointKey = Environment.GetEnvironmentVariable("StripeEndpointSecret"); IPayment._EndpointSecret = string.IsNullOrEmpty(StripeEndpointKey) ? "" : StripeEndpointKey; } //////////////////////////////// /////// Auth Service //////// //////////////////////////////// RsaSecurityKey? PublicKey = null; using (HttpClient client = new HttpClient()) { while (PublicKey == null) { try { HttpResponseMessage PublicKeyResponse = await client.GetAsync("https://auth.mistox.com/api/auth/publickey"); if (PublicKeyResponse.IsSuccessStatusCode) { string publicKey = await PublicKeyResponse.Content.ReadAsStringAsync(); RSA rsa = RSA.Create(); rsa.ImportFromPem(publicKey); PublicKey = new RsaSecurityKey(rsa); } else { await Task.Delay(2000); // sleep the main thread for 2 seconds before sending another request. Prevent DDOS of my own equiptment Console.WriteLine("auth.mistox.com returned error code: " + PublicKeyResponse.StatusCode); } } catch (Exception e) { await Task.Delay(2000); // sleep the main thread for 2 seconds before sending another request. Prevent DDOS of my own equiptment Console.WriteLine("Error loading public key: " + e.InnerException?.Message); } } Console.WriteLine("PublicKey loaded"); } // Pull JWT out of cookie for auth builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }).AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = "https://auth.mistox.com", ValidAudience = "mistox-llc-auth-token", IssuerSigningKey = PublicKey, ClockSkew = TimeSpan.FromMinutes(1) }; options.Events = new JwtBearerEvents { OnMessageReceived = context => { context.Token = context.Request.Cookies["mistox_session"]; return Task.CompletedTask; } }; }); //////////////////////////////// /// Rate Limiting Service //// //////////////////////////////// List allowedOrigins = new List{ "https://boredcareers.com", "https://www.boredcareers.com" }; if (builder.Environment.IsDevelopment()) { allowedOrigins.Add("http://localhost:5000"); } builder.Services.AddCors(options => { options.AddDefaultPolicy(policy => { policy.WithOrigins(allowedOrigins.ToArray()) .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); }); }); builder.Services.AddRateLimiter(options => { options.AddPolicy("PerUserPolicy", httpContext => { var userId = httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? $"ip:{httpContext.Connection.RemoteIpAddress}"; return RateLimitPartition.GetTokenBucketLimiter(userId, key => new TokenBucketRateLimiterOptions { TokenLimit = 10, // max 10 requests QueueProcessingOrder = QueueProcessingOrder.OldestFirst, QueueLimit = 0, ReplenishmentPeriod = TimeSpan.FromSeconds(15), TokensPerPeriod = 2, AutoReplenishment = true }); }); }); //////////////////////////////// ///// Background Services ///// //////////////////////////////// builder.Services.AddHostedService(); //////////////////////////////// ///// ASPNET Core Function ///// //////////////////////////////// builder.Services.AddControllers(); var app = builder.Build(); // Configure the HTTP request pipeline. if( !app.Environment.IsDevelopment() ) { app.UseHsts(); } app.UseDefaultFiles(); app.UseStaticFiles(); app.UseRateLimiter(); app.UseCors(); app.UseRouting(); app.UseAuthentication(); // Autorenew JWT about to expire app.Use(async (context, next) =>{ ClaimsPrincipal user = context.User; if (user.Identity?.IsAuthenticated == true) { string? token = context.Request.Cookies["mistox_session"]; Claim? staySignedIn = user.FindFirst(ClaimTypes.IsPersistent); if (staySignedIn != null && bool.TryParse(staySignedIn.Value, out bool sli) && sli == true) { Claim? expClaim = user.FindFirst(ClaimTypes.Expiration); if (expClaim != null && long.TryParse(expClaim.Value, out long expUnix)) { DateTimeOffset expTime = DateTimeOffset.FromUnixTimeSeconds(expUnix); if ((expTime - DateTimeOffset.UtcNow) < TimeSpan.FromDays(3)) { using (HttpClient client = new HttpClient()) { HttpResponseMessage response = await client.PostAsJsonAsync("https://auth.mistox.com/api/auth/renew", new JWTRenewRequest() { JWT = token }); if (response.IsSuccessStatusCode) { string newJwt = await response.Content.ReadAsStringAsync(); context.Response.Cookies.Append("mistox_session", newJwt, new CookieOptions { HttpOnly = true, Secure = true, SameSite = SameSiteMode.Strict, Expires = DateTimeOffset.UtcNow.AddYears(3) }); } } } } } } else { context.Response.Cookies.Delete("mistox_session"); } await next(); }); app.MapControllers(); app.MapFallbackToFile("index.html"); app.Run();