diff --git a/.gitignore b/.gitignore
index baf319e..3f58456 100644
--- a/.gitignore
+++ b/.gitignore
@@ -210,3 +210,5 @@ $tf/
*.gpState
SfeduSchedule/appsettings.Development.json
+
+.idea/.idea.SfeduSchedule/.idea/
diff --git a/SfeduSchedule/Controllers/ScheduleController.cs b/SfeduSchedule/Controllers/ScheduleController.cs
index 3179a1d..8e54378 100644
--- a/SfeduSchedule/Controllers/ScheduleController.cs
+++ b/SfeduSchedule/Controllers/ScheduleController.cs
@@ -5,12 +5,8 @@ namespace SfeduSchedule.Controllers
{
[ApiController]
[Route("api/[controller]")]
- public class ScheduleController : ControllerBase
+ public class ScheduleController(ModeusService modeusService) : ControllerBase
{
- private readonly ModeusService _modeusService;
- public ScheduleController(ModeusService modeusService) =>
- _modeusService = modeusService;
-
///
/// Получить расписание для указанных пользователей.
///
@@ -18,20 +14,14 @@ namespace SfeduSchedule.Controllers
/// Список событий расписания.
/// Возвращает расписание
[HttpGet]
+ [Route("test")]
public async Task Get([FromQuery] List attendeePersonId, [FromQuery] DateTime? startDate, [FromQuery] DateTime? endDate)
{
- if (!startDate.HasValue)
- {
- startDate = DateTime.UtcNow;
- }
-
- if (!endDate.HasValue)
- {
- endDate = DateTime.UtcNow.AddDays(20);
- }
+ startDate ??= DateTime.UtcNow;
+ endDate ??= DateTime.UtcNow.AddDays(20);
var msr = new ModeusScheduleRequest(500, (DateTime)startDate, (DateTime)endDate, attendeePersonId);
- var schedule = await _modeusService.GetScheduleAsync(msr);
+ var schedule = await modeusService.GetScheduleAsync(msr);
return Ok(schedule);
}
@@ -44,7 +34,7 @@ namespace SfeduSchedule.Controllers
[HttpPost]
public async Task Post([FromBody] ModeusScheduleRequest request)
{
- var schedule = await _modeusService.GetScheduleAsync(request);
+ var schedule = await modeusService.GetScheduleAsync(request);
return Ok(schedule);
}
}
diff --git a/SfeduSchedule/GlobalVariables.cs b/SfeduSchedule/GlobalVariables.cs
new file mode 100644
index 0000000..8c64349
--- /dev/null
+++ b/SfeduSchedule/GlobalVariables.cs
@@ -0,0 +1,7 @@
+namespace SfeduSchedule
+{
+ public static class GlobalVariables
+ {
+ public static string JwtFilePath { get; set; } = "data/jwt.txt";
+ }
+}
\ No newline at end of file
diff --git a/SfeduSchedule/Jobs/UpdateJWTJob.cs b/SfeduSchedule/Jobs/UpdateJWTJob.cs
new file mode 100644
index 0000000..6519521
--- /dev/null
+++ b/SfeduSchedule/Jobs/UpdateJWTJob.cs
@@ -0,0 +1,86 @@
+using Microsoft.Playwright;
+using Quartz;
+
+namespace SfeduSchedule.Jobs;
+
+public class UpdateJwtJob(IConfiguration configuration, ILogger logger) : IJob
+{
+ public async Task Execute(IJobExecutionContext jobContext)
+ {
+ logger.LogInformation("Начало выполнения UpdateJwtJob");
+
+ string? username = configuration["MS_USERNAME"];
+ string? password = configuration["MS_PASSWORD"];
+
+ if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
+ {
+ logger.LogError("Не указаны учетные данные для входа");
+ return;
+ }
+
+ using var playwright = await Playwright.CreateAsync();
+ await using var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
+ {
+ Headless = true
+ });
+ var context = await browser.NewContextAsync(new BrowserNewContextOptions
+ {
+ ViewportSize = null
+ });
+ var page = await context.NewPageAsync();
+
+ try
+ {
+ logger.LogInformation("Начало выполнения авторизации Microsoft");
+ await MicrosoftLoginHelper.LoginMicrosoftAsync(page, username, password);
+
+ var sessionStorageJson = await page.EvaluateAsync(@"
+ JSON.stringify(sessionStorage)
+ ");
+
+ // Извлечение id_token из sessionStorageJson
+ string? idToken = null;
+ try
+ {
+ var sessionStorageDict = System.Text.Json.JsonSerializer.Deserialize>(sessionStorageJson);
+ if (sessionStorageDict != null)
+ {
+ var oidcKey = sessionStorageDict.Keys.FirstOrDefault(k => k.StartsWith("oidc.user:"));
+ if (oidcKey != null)
+ {
+ var oidcValueJson = sessionStorageDict[oidcKey]?.ToString();
+ if (!string.IsNullOrEmpty(oidcValueJson))
+ {
+ using var doc = System.Text.Json.JsonDocument.Parse(oidcValueJson);
+ if (doc.RootElement.TryGetProperty("id_token", out var idTokenElement))
+ {
+ idToken = idTokenElement.GetString();
+ }
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Ошибка при извлечении id_token из sessionStorageJson");
+ return;
+ }
+
+ configuration["TOKEN"] = idToken;
+
+ await File.WriteAllTextAsync(GlobalVariables.JwtFilePath, idToken + "\n" + DateTime.Now.ToString("O"));
+
+ logger.LogInformation("UpdateJwtJob выполнен успешно");
+
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Ошибка при выполнении UpdateJwtJob");
+ }
+ finally
+ {
+ await context.CloseAsync();
+ await browser.CloseAsync();
+ }
+ }
+}
\ No newline at end of file
diff --git a/SfeduSchedule/MicrosoftLoginHelper.cs b/SfeduSchedule/MicrosoftLoginHelper.cs
new file mode 100644
index 0000000..2b107b4
--- /dev/null
+++ b/SfeduSchedule/MicrosoftLoginHelper.cs
@@ -0,0 +1,56 @@
+using Microsoft.Playwright;
+using System.Text.RegularExpressions;
+
+public static class MicrosoftLoginHelper
+{
+ private static readonly string LoginUrl = "https://sfedu.modeus.org/";
+ // private static readonly string StorageStatePath = "ms_storage_state.json";
+
+ public static async Task LoginMicrosoftAsync(IPage page, string username, string password)
+ {
+ if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
+ throw new Exception("username и password обязательны для авторизации Microsoft");
+
+ await page.GotoAsync(LoginUrl, new PageGotoOptions { WaitUntil = WaitUntilState.DOMContentLoaded });
+
+ await page.WaitForURLAsync(new Regex("login\\.(microsoftonline|live)\\.com", RegexOptions.IgnoreCase), new PageWaitForURLOptions { Timeout = 60_000 });
+
+ var useAnotherAccount = page.Locator("div#otherTile, #otherTileText, div[data-test-id='useAnotherAccount']");
+ if (await useAnotherAccount.First.IsVisibleAsync(new LocatorIsVisibleOptions { Timeout = 2000 }))
+ await useAnotherAccount.First.ClickAsync();
+
+ var emailInput = page.Locator("input[name='loginfmt'], input#i0116");
+ await emailInput.WaitForAsync(new LocatorWaitForOptions { State = WaitForSelectorState.Visible, Timeout = 30_000 });
+ await emailInput.FillAsync(username);
+
+ var nextButton = page.Locator("#idSIButton9, input#idSIButton9");
+ await nextButton.ClickAsync();
+
+ var passwordInput = page.Locator("input[name='passwd'], input#i0118");
+ await passwordInput.WaitForAsync(new LocatorWaitForOptions { State = WaitForSelectorState.Visible, Timeout = 30_000 });
+ await passwordInput.FillAsync(password);
+ await nextButton.ClickAsync();
+
+ await page.WaitForSelectorAsync("button, input[type='submit'], a", new PageWaitForSelectorOptions { Timeout = 8000 });
+
+ var kmsiYesNoVisible = await page.Locator("#idSIButton9, #idBtn_Back").First.IsVisibleAsync(new LocatorIsVisibleOptions { Timeout = 3000 });
+ if (kmsiYesNoVisible)
+ {
+ var noBtn = page.Locator("#idBtn_Back");
+ if (await noBtn.IsVisibleAsync())
+ await noBtn.ClickAsync();
+ else
+ await page.Locator("#idSIButton9").ClickAsync();
+ }
+
+ await page.WaitForURLAsync(url => !Regex.IsMatch(new Uri(url).Host, "login\\.(microsoftonline|live)\\.com", RegexOptions.IgnoreCase), new PageWaitForURLOptions { Timeout = 60_000 });
+ await page.WaitForLoadStateAsync(LoadState.NetworkIdle);
+
+ // Сохраняем storage state после успешного входа
+ // await page.Context.StorageStateAsync(new BrowserContextStorageStateOptions { Path = StorageStatePath });
+
+ var currentHost = new Uri(page.Url).Host;
+ if (Regex.IsMatch(currentHost, "login\\.(microsoftonline|live)\\.com", RegexOptions.IgnoreCase))
+ throw new Exception("Авторизация не завершена: остались на странице Microsoft Login");
+ }
+}
diff --git a/SfeduSchedule/Program.cs b/SfeduSchedule/Program.cs
index f53423c..d726649 100644
--- a/SfeduSchedule/Program.cs
+++ b/SfeduSchedule/Program.cs
@@ -1,35 +1,92 @@
-using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Identity.Web;
+using Quartz;
using Scalar.AspNetCore;
+using SfeduSchedule;
+using SfeduSchedule.Jobs;
using SfeduSchedule.Services;
var builder = WebApplication.CreateBuilder(args);
var configuration = builder.Configuration;
-string? adminToken = configuration["TOKEN"];
+string? preinstsalledJwtToken = configuration["TOKEN"];
+string updateJwtCron = configuration["UPDATE_JWT_CRON"] ?? "0 4 * ? * *";
-if (string.IsNullOrEmpty(adminToken))
+// создать папку data если не существует
+var dataDirectory = Path.Combine(AppContext.BaseDirectory, "data");
+if (!Directory.Exists(dataDirectory))
{
- Console.WriteLine("Токен администратора не установлен");
- Environment.Exit(1);
+ Directory.CreateDirectory(dataDirectory);
}
+GlobalVariables.JwtFilePath = Path.Combine(dataDirectory, "jwt.txt");
builder.Services.AddOpenApi();
builder.Services.AddControllers();
builder.Services.AddHttpClient();
-builder.Services.Configure(options =>
-{
- options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
-});
-
builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration);
+var jobKey = new JobKey("UpdateJWTJob");
+
+if (string.IsNullOrEmpty(preinstsalledJwtToken))
+{
+ builder.Services.AddQuartz(q =>
+ {
+ q.AddJob(opts => opts.WithIdentity(jobKey));
+
+ q.AddTrigger(opts => opts
+ .ForJob(jobKey)
+ .WithIdentity("UpdateJWTJob-trigger")
+ .WithCronSchedule(updateJwtCron)
+ );
+ });
+
+ builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
+}
+
var app = builder.Build();
-app.UseForwardedHeaders();
+var logger = app.Services.GetRequiredService>();
-app.MapOpenApi();
+if (string.IsNullOrEmpty(preinstsalledJwtToken))
+{
+ var schedulerFactory = app.Services.GetRequiredService();
+ 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("Используем существующий токен: {Token}", token);
+ configuration["TOKEN"] = token;
+ }
+ else
+ {
+ logger.LogInformation("Токен истек или скоро истечет, выполняем обновление токена");
+ await scheduler.TriggerJob(jobKey);
+ }
+ }
+ else
+ {
+ logger.LogInformation("Файл jwt.txt не содержит дату истечения или она некорректна, выполняем обновление токена");
+ await scheduler.TriggerJob(jobKey);
+ }
+ }
+ else
+ await scheduler.TriggerJob(jobKey);
+}
+
+
+if (app.Environment.IsDevelopment())
+{
+ app.MapOpenApi();
+}
app.MapScalarApiReference(options =>
{
options.WithTitle("Расписание занятий ЮФУ");
diff --git a/SfeduSchedule/SfeduSchedule.csproj b/SfeduSchedule/SfeduSchedule.csproj
index d3a2b4f..92e0db1 100644
--- a/SfeduSchedule/SfeduSchedule.csproj
+++ b/SfeduSchedule/SfeduSchedule.csproj
@@ -5,11 +5,15 @@
enable
enable
Linux
+ true
+ $(NoWarn);CS1591;CS1573
+
+