From 347104b8489f9e752c1807038744a37c7f24e6d4 Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Fri, 5 Sep 2025 18:51:43 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D0=B0=D0=B2=D1=82=D0=BE=D0=BC=D0=B0=D1=82=D0=B8=D1=87=D0=B5?= =?UTF-8?q?=D1=81=D0=BA=D0=BE=D0=B5=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=82=D0=BE=D0=BA=D0=B5=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + .../Controllers/ScheduleController.cs | 22 ++--- SfeduSchedule/GlobalVariables.cs | 7 ++ SfeduSchedule/Jobs/UpdateJWTJob.cs | 86 +++++++++++++++++++ SfeduSchedule/MicrosoftLoginHelper.cs | 56 ++++++++++++ SfeduSchedule/Program.cs | 81 ++++++++++++++--- SfeduSchedule/SfeduSchedule.csproj | 4 + 7 files changed, 230 insertions(+), 28 deletions(-) create mode 100644 SfeduSchedule/GlobalVariables.cs create mode 100644 SfeduSchedule/Jobs/UpdateJWTJob.cs create mode 100644 SfeduSchedule/MicrosoftLoginHelper.cs 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 + +