Files
SfeduSchedule/SfeduSchedule/Program.cs
Sergey Karmanov 82e7d92584
All checks were successful
Create and publish a Docker image / Publish image (push) Successful in 3m54s
Добавил получение guid пользователя по фио через токен
2025-09-08 16:52:14 +03:00

216 lines
8.1 KiB
C#

using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;
using Quartz;
using SfeduSchedule;
using SfeduSchedule.Jobs;
using SfeduSchedule.Services;
using X.Extensions.Logging.Telegram.Extensions;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using SfeduSchedule.Auth;
var builder = WebApplication.CreateBuilder(args);
var configuration = builder.Configuration;
string? preinstalledJwtToken = configuration["TOKEN"];
string? tgChatId = configuration["TG_CHAT_ID"];
string? tgToken = configuration["TG_TOKEN"];
string updateJwtCron = configuration["UPDATE_JWT_CRON"] ?? "0 0 4 ? * *";
int permitLimit = int.TryParse(configuration["PERMIT_LIMIT"], out var parsedPermitLimit) ? parsedPermitLimit : 40;
int timeLimit = int.TryParse(configuration["TIME_LIMIT"], out var parsedTimeLimit) ? parsedTimeLimit : 10;
// создать папку data если не существует
var dataDirectory = Path.Combine(AppContext.BaseDirectory, "data");
if (!Directory.Exists(dataDirectory))
{
Directory.CreateDirectory(dataDirectory);
}
GlobalVariables.JwtFilePath = Path.Combine(dataDirectory, "jwt.txt");
var pluginsPath = Path.Combine(dataDirectory, "Plugins");
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
if (!string.IsNullOrEmpty(tgChatId) && !string.IsNullOrEmpty(tgToken))
builder.Logging.AddTelegram(options =>
{
options.ChatId = tgChatId;
options.AccessToken = tgToken;
options.FormatterConfiguration.UseEmoji = true;
options.FormatterConfiguration.ReadableApplicationName = "Sfedu Schedule";
options.LogLevel = new Dictionary<string, LogLevel>
{
{ "Default", LogLevel.Error },
{ "SfeduSchedule.Jobs.UpdateJwtJob", LogLevel.Information },
{ "Program", LogLevel.Information }
};
});
// Включаем MVC контроллеры
var mvcBuilder = builder.Services.AddControllers();
builder.Services.AddHttpClient<ModeusService>();
builder.Services.AddAuthentication()
.AddScheme<AuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(
ApiKeyAuthenticationDefaults.Scheme, _ => { });
builder.Services.AddAuthorization();
builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration);
// Загружаем плагины (изолированно), даём им зарегистрировать DI и подключаем контроллеры
var loaded = PluginLoader.LoadPlugins(pluginsPath);
foreach (var p in loaded)
{
// DI из плагина
p.Instance.ConfigureServices(builder.Services);
// Подключаем контроллеры плагина
mvcBuilder.PartManager.ApplicationParts.Add(new AssemblyPart(p.Assembly));
}
var jobKey = new JobKey("UpdateJWTJob");
if (string.IsNullOrEmpty(preinstalledJwtToken))
{
builder.Services.AddQuartz(q =>
{
q.AddJob<UpdateJwtJob>(opts => opts.WithIdentity(jobKey));
q.AddTrigger(opts => opts
.ForJob(jobKey)
.WithIdentity("UpdateJWTJob-trigger")
.WithCronSchedule(updateJwtCron)
);
});
builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
}
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
var xmlFile = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
options.IncludeXmlComments(xmlPath);
// Добавляем только схему авторизации по ApiKey
options.AddSecurityDefinition(ApiKeyAuthenticationDefaults.Scheme, new Microsoft.OpenApi.Models.OpenApiSecurityScheme
{
Description = $"Api Key needed to access the endpoints. {ApiKeyAuthenticationDefaults.HeaderName}: Your_API_Key",
Name = ApiKeyAuthenticationDefaults.HeaderName,
In = Microsoft.OpenApi.Models.ParameterLocation.Header,
Type = Microsoft.OpenApi.Models.SecuritySchemeType.ApiKey,
Scheme = ApiKeyAuthenticationDefaults.Scheme
});
options.OperationFilter<SwaggerAuthorizeOperationFilter>();
});
builder.Services.AddRateLimiter(options =>
{
options.AddPolicy("throttle", httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: (httpContext.Request.Headers.TryGetValue("X-Forwarded-For", out var xff) && !string.IsNullOrWhiteSpace(xff.ToString()))
? xff.ToString().Split(',')[0].Trim()
: (httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown"),
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = permitLimit,
Window = TimeSpan.FromSeconds(timeLimit)
}));
options.OnRejected = async (context, cancellationToken) =>
{
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
context.HttpContext.Response.Headers["Retry-After"] = timeLimit.ToString();
await context.HttpContext.Response.WriteAsync("Rate limit exceeded. Please try again later.",
cancellationToken);
var reqLogger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Program>>();
var clientIp = (context.HttpContext.Request.Headers.TryGetValue("X-Forwarded-For", out var xff) && !string.IsNullOrWhiteSpace(xff.ToString()))
? xff.ToString().Split(',')[0].Trim()
: context.HttpContext.Connection.RemoteIpAddress?.ToString();
reqLogger.LogWarning("Rate limit exceeded for IP: {IpAddress}", clientIp);
};
});
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor |
ForwardedHeaders.XForwardedProto;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
var app = builder.Build();
var logger = app.Services.GetRequiredService<ILogger<Program>>();
app.UseForwardedHeaders();
if (string.IsNullOrEmpty(preinstalledJwtToken))
{
var schedulerFactory = app.Services.GetRequiredService<ISchedulerFactory>();
var scheduler = await schedulerFactory.GetScheduler();
// Проверить существование файла jwt.txt
if (File.Exists(GlobalVariables.JwtFilePath))
{
logger.LogInformation("Обнаружена прошлая сессия");
var lines = await File.ReadAllLinesAsync(GlobalVariables.JwtFilePath);
if (lines.Length > 1 && DateTime.TryParse(lines[1], out var expirationDate))
{
logger.LogInformation("Дата истечения токена: {ExpirationDate}", expirationDate);
if (expirationDate.AddHours(23) > DateTime.Now)
{
var token = lines[0];
logger.LogInformation("Используем существующий токен");
configuration["TOKEN"] = token;
}
else
{
logger.LogInformation("Токен истек или скоро истечет, выполняем обновление токена");
await scheduler.TriggerJob(jobKey);
}
}
else
{
logger.LogInformation(
"Файл jwt.txt не содержит дату истечения или она некорректна, выполняем обновление токена");
await scheduler.TriggerJob(jobKey);
}
}
else
await scheduler.TriggerJob(jobKey);
}
app.UseSwagger();
app.UseSwaggerUI();
app.UseAuthentication();
app.UseAuthorization();
app.UseStaticFiles();
app.MapGet("/", async context =>
{
context.Response.ContentType = "text/html; charset=utf-8";
await context.Response.SendFileAsync(Path.Combine(app.Environment.WebRootPath, "index.html"));
});
app.MapControllers();
// Маршруты Minimal API из плагинов
foreach (var p in loaded)
{
logger.LogInformation("Mapping endpoints for plugin: {PluginName}", p.Instance.Name);
p.Instance.MapEndpoints(app);
}
app.UseRateLimiter();
app.Run();