feat: добавлен фильтр для скрытия ссылок на занятия от неавторизованных пользователей

This commit is contained in:
serega404 committed 2026-09-15 19:52:19 +03:00
1 parent 03560b0dc8
commit cfd9196083
3 files changed
+68 -1

No files matched your search

+4
View File
@@ -47,6 +47,10 @@
- `POST /api/proxy/events/search` — получить расписание; - `POST /api/proxy/events/search` — получить расписание;
- `POST /api/proxy/rooms/search` — искать аудитории. - `POST /api/proxy/rooms/search` — искать аудитории.
В ответе `POST /api/proxy/events/search` ссылки на подключение к занятиям защищены. Если `customLocation` содержит абсолютный URI отдельно, внутри текста или Markdown-разметки, пользователь без активной Microsoft-сессии получит значение `авторизуйтесь для просмотра` вместо всего содержимого поля. После входа через Microsoft API возвращает исходное значение. `X-Api-Key` не открывает доступ к ссылкам.
Фильтрация реализована в `ScheduleLocationFilter`. Плагины могут вызывать этот общий helper для своих JSON-ответов, не дублируя проверку авторизации и обработку `customLocation`.
Если `Modeus` отвечает ошибкой, сервис возвращает соответствующий HTTP-статус и текст с префиксом `Proxied Modeus:`. Если `Modeus` отвечает ошибкой, сервис возвращает соответствующий HTTP-статус и текст с префиксом `Proxied Modeus:`.
### 2. Работа с расписанием ### 2. Работа с расписанием
+2 -1
View File
@@ -38,6 +38,7 @@ public class ProxyController(ModeusService modeusService, ILogger<ProxyControlle
"Proxied Modeus: " + e.Message); "Proxied Modeus: " + e.Message);
} }
schedule = await ScheduleLocationFilter.FilterAsync(schedule, HttpContext);
return Ok(schedule); return Ok(schedule);
} }
@@ -67,4 +68,4 @@ public class ProxyController(ModeusService modeusService, ILogger<ProxyControlle
return Ok(rooms); return Ok(rooms);
} }
} }
@@ -0,0 +1,62 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
namespace SfeduSchedule.Services;
/// <summary>
/// Скрывает ссылки на подключение к занятиям от пользователей без Microsoft-сессии.
/// </summary>
public static class ScheduleLocationFilter
{
private const string AuthorizationRequiredMessage = "авторизуйтесь для просмотра";
private static readonly Regex AbsoluteUriPattern = new(
@"(?<![A-Za-z0-9+.-])[A-Za-z][A-Za-z0-9+.-]*:[^\s<>\[\]()]+",
RegexOptions.CultureInvariant);
/// <summary>
/// Возвращает исходное расписание авторизованному пользователю или заменяет значение
/// <c>customLocation</c> безопасной заглушкой, если оно содержит абсолютный URI.
/// </summary>
/// <param name="schedule">JSON расписания Modeus.</param>
/// <param name="httpContext">Контекст запроса, содержащий Microsoft authentication cookie.</param>
/// <returns>Исходный либо отфильтрованный JSON; <see langword="null"/>, если расписание отсутствует.</returns>
/// <exception cref="JsonException">JSON расписания пуст или имеет некорректный формат.</exception>
public static async Task<string?> FilterAsync(string? schedule, HttpContext httpContext)
{
if (schedule == null)
return null;
// API key не даёт доступ к ссылкам: проверяем только cookie, созданную Microsoft OIDC-входом.
var authenticationResult =
await httpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
if (authenticationResult.Succeeded)
return schedule;
var root = JsonNode.Parse(schedule) ?? throw new JsonException("Schedule response is empty.");
if (root["_embedded"]?["event-locations"] is not JsonArray eventLocations)
return root.ToJsonString();
// Если ссылка окружена текстом или Markdown-разметкой, скрываем всё значение customLocation.
foreach (var eventLocation in eventLocations.OfType<JsonObject>())
{
if (eventLocation["customLocation"] is not JsonValue value ||
!value.TryGetValue<string>(out var customLocation) ||
string.IsNullOrWhiteSpace(customLocation) ||
!ContainsAbsoluteUri(customLocation))
continue;
eventLocation["customLocation"] = AuthorizationRequiredMessage;
}
return root.ToJsonString();
}
private static bool ContainsAbsoluteUri(string value)
{
return AbsoluteUriPattern.Matches(value)
.Any(match => Uri.TryCreate(match.Value, UriKind.Absolute, out _));
}
}