96 lines
4.0 KiB
C#
96 lines
4.0 KiB
C#
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<AppDbContext>(options =>
|
|
options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString)));
|
|
|
|
// ── 포트폴리오 1) 관리자 비밀번호 검증 도우미 (싱글턴) ───────────────────────
|
|
builder.Services.AddSingleton<PortfolioAdminAuth>();
|
|
|
|
// ── 포트폴리오 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<FormOptions>(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<ILoggerFactory>().CreateLogger("PortfolioSchema"));
|
|
|
|
app.Run();
|