Добавил автоматическое получение токена
All checks were successful
Create and publish a Docker image / Publish image (push) Successful in 3m0s
All checks were successful
Create and publish a Docker image / Publish image (push) Successful in 3m0s
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -210,3 +210,5 @@ $tf/
|
|||||||
*.gpState
|
*.gpState
|
||||||
|
|
||||||
SfeduSchedule/appsettings.Development.json
|
SfeduSchedule/appsettings.Development.json
|
||||||
|
|
||||||
|
.idea/.idea.SfeduSchedule/.idea/
|
||||||
|
@@ -5,12 +5,8 @@ namespace SfeduSchedule.Controllers
|
|||||||
{
|
{
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/[controller]")]
|
[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>
|
||||||
/// Получить расписание для указанных пользователей.
|
/// Получить расписание для указанных пользователей.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -18,20 +14,14 @@ namespace SfeduSchedule.Controllers
|
|||||||
/// <returns>Список событий расписания.</returns>
|
/// <returns>Список событий расписания.</returns>
|
||||||
/// <response code="200">Возвращает расписание</response>
|
/// <response code="200">Возвращает расписание</response>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
|
[Route("test")]
|
||||||
public async Task<IActionResult> Get([FromQuery] List<Guid> attendeePersonId, [FromQuery] DateTime? startDate, [FromQuery] DateTime? endDate)
|
public async Task<IActionResult> Get([FromQuery] List<Guid> attendeePersonId, [FromQuery] DateTime? startDate, [FromQuery] DateTime? endDate)
|
||||||
{
|
{
|
||||||
if (!startDate.HasValue)
|
startDate ??= DateTime.UtcNow;
|
||||||
{
|
endDate ??= DateTime.UtcNow.AddDays(20);
|
||||||
startDate = DateTime.UtcNow;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!endDate.HasValue)
|
|
||||||
{
|
|
||||||
endDate = DateTime.UtcNow.AddDays(20);
|
|
||||||
}
|
|
||||||
|
|
||||||
var msr = new ModeusScheduleRequest(500, (DateTime)startDate, (DateTime)endDate, attendeePersonId);
|
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);
|
return Ok(schedule);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +34,7 @@ namespace SfeduSchedule.Controllers
|
|||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> Post([FromBody] ModeusScheduleRequest request)
|
public async Task<IActionResult> Post([FromBody] ModeusScheduleRequest request)
|
||||||
{
|
{
|
||||||
var schedule = await _modeusService.GetScheduleAsync(request);
|
var schedule = await modeusService.GetScheduleAsync(request);
|
||||||
return Ok(schedule);
|
return Ok(schedule);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
7
SfeduSchedule/GlobalVariables.cs
Normal file
7
SfeduSchedule/GlobalVariables.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace SfeduSchedule
|
||||||
|
{
|
||||||
|
public static class GlobalVariables
|
||||||
|
{
|
||||||
|
public static string JwtFilePath { get; set; } = "data/jwt.txt";
|
||||||
|
}
|
||||||
|
}
|
86
SfeduSchedule/Jobs/UpdateJWTJob.cs
Normal file
86
SfeduSchedule/Jobs/UpdateJWTJob.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
SfeduSchedule/MicrosoftLoginHelper.cs
Normal file
56
SfeduSchedule/MicrosoftLoginHelper.cs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
@@ -1,35 +1,92 @@
|
|||||||
using Microsoft.AspNetCore.HttpOverrides;
|
|
||||||
using Microsoft.Identity.Web;
|
using Microsoft.Identity.Web;
|
||||||
|
using Quartz;
|
||||||
using Scalar.AspNetCore;
|
using Scalar.AspNetCore;
|
||||||
|
using SfeduSchedule;
|
||||||
|
using SfeduSchedule.Jobs;
|
||||||
using SfeduSchedule.Services;
|
using SfeduSchedule.Services;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
var configuration = builder.Configuration;
|
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("Токен администратора не установлен");
|
Directory.CreateDirectory(dataDirectory);
|
||||||
Environment.Exit(1);
|
|
||||||
}
|
}
|
||||||
|
GlobalVariables.JwtFilePath = Path.Combine(dataDirectory, "jwt.txt");
|
||||||
|
|
||||||
builder.Services.AddOpenApi();
|
builder.Services.AddOpenApi();
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
builder.Services.AddHttpClient<ModeusService>();
|
builder.Services.AddHttpClient<ModeusService>();
|
||||||
|
|
||||||
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration);
|
||||||
|
|
||||||
|
var jobKey = new JobKey("UpdateJWTJob");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(preinstsalledJwtToken))
|
||||||
{
|
{
|
||||||
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
builder.Services.AddQuartz(q =>
|
||||||
|
{
|
||||||
|
q.AddJob<UpdateJwtJob>(opts => opts.WithIdentity(jobKey));
|
||||||
|
|
||||||
|
q.AddTrigger(opts => opts
|
||||||
|
.ForJob(jobKey)
|
||||||
|
.WithIdentity("UpdateJWTJob-trigger")
|
||||||
|
.WithCronSchedule(updateJwtCron)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddMicrosoftIdentityWebAppAuthentication(builder.Configuration);
|
builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
|
||||||
|
}
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
app.UseForwardedHeaders();
|
var logger = app.Services.GetRequiredService<ILogger<Program>>();
|
||||||
|
|
||||||
|
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.MapOpenApi();
|
||||||
|
}
|
||||||
app.MapScalarApiReference(options =>
|
app.MapScalarApiReference(options =>
|
||||||
{
|
{
|
||||||
options.WithTitle("Расписание занятий ЮФУ");
|
options.WithTitle("Расписание занятий ЮФУ");
|
||||||
|
@@ -5,11 +5,15 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<NoWarn>$(NoWarn);CS1591;CS1573</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.8"/>
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.8"/>
|
||||||
<PackageReference Include="Microsoft.Identity.Web" Version="3.14.0" />
|
<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" />
|
<PackageReference Include="Scalar.AspNetCore" Version="2.7.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user