Добавил автоматическое получение токена
All checks were successful
Create and publish a Docker image / Publish image (push) Successful in 3m0s

This commit is contained in:
2025-09-05 18:51:43 +03:00
parent 3ac1fefcfe
commit 347104b848
7 changed files with 230 additions and 28 deletions

2
.gitignore vendored
View File

@@ -210,3 +210,5 @@ $tf/
*.gpState
SfeduSchedule/appsettings.Development.json
.idea/.idea.SfeduSchedule/.idea/

View File

@@ -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;
/// <summary>
/// Получить расписание для указанных пользователей.
/// </summary>
@@ -18,20 +14,14 @@ namespace SfeduSchedule.Controllers
/// <returns>Список событий расписания.</returns>
/// <response code="200">Возвращает расписание</response>
[HttpGet]
[Route("test")]
public async Task<IActionResult> Get([FromQuery] List<Guid> 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<IActionResult> Post([FromBody] ModeusScheduleRequest request)
{
var schedule = await _modeusService.GetScheduleAsync(request);
var schedule = await modeusService.GetScheduleAsync(request);
return Ok(schedule);
}
}

View File

@@ -0,0 +1,7 @@
namespace SfeduSchedule
{
public static class GlobalVariables
{
public static string JwtFilePath { get; set; } = "data/jwt.txt";
}
}

View File

@@ -0,0 +1,86 @@
using Microsoft.Playwright;
using Quartz;
namespace SfeduSchedule.Jobs;
public class UpdateJwtJob(IConfiguration configuration, ILogger<UpdateJwtJob> 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<string>(@"
JSON.stringify(sessionStorage)
");
// Извлечение id_token из sessionStorageJson
string? idToken = null;
try
{
var sessionStorageDict = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(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();
}
}
}

View File

@@ -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");
}
}

View File

@@ -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<ModeusService>();
builder.Services.Configure<ForwardedHeadersOptions>(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<UpdateJwtJob>(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<ILogger<Program>>();
app.MapOpenApi();
if (string.IsNullOrEmpty(preinstsalledJwtToken))
{
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("Используем существующий токен: {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("Расписание занятий ЮФУ");

View File

@@ -5,11 +5,15 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591;CS1573</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.8"/>
<PackageReference Include="Microsoft.Identity.Web" Version="3.14.0" />
<PackageReference Include="Microsoft.Playwright" Version="1.55.0" />
<PackageReference Include="Quartz.AspNetCore" Version="3.15.0" />
<PackageReference Include="Scalar.AspNetCore" Version="2.7.2" />
</ItemGroup>