using Microsoft.EntityFrameworkCore; using GameServer.Data; using GameServer.Services; using Scalar.AspNetCore; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.RateLimiting; using System.Threading.RateLimiting; using System.Security.Claims; var builder = WebApplication.CreateBuilder(args); // appsettings.json에서 연결 문자열(Connection String) 가져오기 var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); // MariaDB(Pomelo) 엔진 등록 builder.Services.AddDbContext(options => options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString))); // ── 포트폴리오 1) 관리자 비밀번호 검증 도우미 (싱글턴) ─────────────────────── builder.Services.AddSingleton(); // ── 포트폴리오 2) 관리자 전용 쿠키 인증 / 인가 정책 ───────────────────────── builder.Services.AddAuthentication(PortfolioAdminAuth.CookieScheme) .AddCookie(PortfolioAdminAuth.CookieScheme, o => { o.Cookie.Name = "portfolio_admin"; o.Cookie.HttpOnly = true; o.Cookie.SameSite = SameSiteMode.Lax; o.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; o.ExpireTimeSpan = TimeSpan.FromDays(7); o.SlidingExpiration = true; // API 서버이므로 로그인 페이지로 리다이렉트하지 않고 상태 코드만 돌려준다. o.Events.OnRedirectToLogin = ctx => { ctx.Response.StatusCode = 401; return Task.CompletedTask; }; o.Events.OnRedirectToAccessDenied = ctx => { ctx.Response.StatusCode = 403; return Task.CompletedTask; }; }); builder.Services.AddAuthorization(o => o.AddPolicy(PortfolioAdminAuth.PolicyName, p => p.AddAuthenticationSchemes(PortfolioAdminAuth.CookieScheme).RequireAuthenticatedUser())); // ── 포트폴리오 3) 로그인 무차별 대입 방지: IP별 고정 창(5분에 8회) ────────── builder.Services.AddRateLimiter(options => { options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; options.AddPolicy("portfolio-login", httpContext => RateLimitPartition.GetFixedWindowLimiter( partitionKey: httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown", factory: _ => new FixedWindowRateLimiterOptions { PermitLimit = 8, Window = TimeSpan.FromMinutes(5), QueueLimit = 0, QueueProcessingOrder = QueueProcessingOrder.OldestFirst, AutoReplenishment = true })); }); builder.Services.AddControllers(); builder.Services.AddOpenApi(); // ── 포트폴리오 5) 이미지 업로드용 멀티파트 본문 제한 (100MB) ──────────────── builder.Services.Configure(o => o.MultipartBodyLengthLimit = 100L * 1024 * 1024); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.MapOpenApi(); app.MapScalarApiReference(); } app.UseHttpsRedirection(); // ── 포트폴리오 6) 정적 사이트 + 레이트 리미터 + 인증 파이프라인 ───────────── app.UseDefaultFiles(); // "/" 요청을 index.html 로 연결 (UseStaticFiles 앞에 와야 한다) app.UseStaticFiles(); app.UseRateLimiter(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); // ── 포트폴리오 7) SPA 딥링크 대응. // MapControllers 뒤에 있으므로 /myGame/* 와 /api/* 라우팅을 가리지 않는다. app.MapFallbackToFile("index.html"); // ── 포트폴리오 8) 포트폴리오 테이블 생성 + 시드. // 실패해도 예외를 던지지 않으므로 게임 서버 부팅을 막지 않는다. await PortfolioSchemaInitializer.EnsureCreatedAsync(app.Services, app.Services.GetRequiredService().CreateLogger("PortfolioSchema")); app.Run();