Compare commits
10 Commits
9fd446fe04
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1974f36b24 | |||
| a75c952f45 | |||
| be1e2041ff | |||
| e237a248ca | |||
| d2cba1fb34 | |||
| 24eee21b20 | |||
| 7a1eddbe51 | |||
| 0b329ec9ec | |||
| f7bc7af9e3 | |||
| b85fdc777a |
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#
|
||||
# https://help.github.com/articles/dealing-with-line-endings/
|
||||
#
|
||||
# These are explicitly windows files and should use crlf
|
||||
|
||||
* text=auto eol=lf
|
||||
|
||||
*.bat text eol=crlf
|
||||
*.nsi text eol=crlf
|
||||
+1
-1
Submodule ModeusSchedule.MSAuth updated: 5b906d6d07...89316e247f
@@ -1,8 +1,362 @@
|
||||
# Прокси для расписания в Modeus
|
||||
# SfeduSchedule
|
||||
|
||||
## TODO
|
||||
`SfeduSchedule` это ASP.NET Core-приложение, которое работает как прокси и вспомогательный API поверх `Modeus`. Проект умеет:
|
||||
|
||||
- [x] Добавить RateLimiter
|
||||
- [x] Добавить обработку ошибок при запросах к modeus
|
||||
- [ ] Добавить кэширование расписания
|
||||
- [ ] Сделать передачу ошибок выше по цепочке
|
||||
- проксировать запросы к расписанию и поиску аудиторий в `Modeus`;
|
||||
- отдавать расписание в `ICS`;
|
||||
- искать GUID пользователя по ФИО;
|
||||
- поддерживать веб-вход через Microsoft для получения GUID текущего пользователя;
|
||||
- периодически обновлять JWT и локальный индекс сотрудников;
|
||||
- публиковать метрики Prometheus;
|
||||
- загружать внешние плагины из `data/Plugins`.
|
||||
|
||||
Основной проект находится в [SfeduSchedule/SfeduSchedule.csproj](/home/serega404/Рабочий стол/SfeduSchedule/SfeduSchedule/SfeduSchedule.csproj), общие DTO и контракт плагинов в [ModeusSchedule.Abstractions](/home/serega404/Рабочий стол/SfeduSchedule/ModeusSchedule.Abstractions), пример плагина в [SfeduSchedule.Plugin.Sample](/home/serega404/Рабочий стол/SfeduSchedule/SfeduSchedule.Plugin.Sample).
|
||||
|
||||
## Как это работает
|
||||
|
||||
На старте приложение:
|
||||
|
||||
1. Загружает конфигурацию из `appsettings`, переменных окружения и других стандартных источников .NET.
|
||||
2. Проставляет значения по умолчанию для части переменных, если они не заданы.
|
||||
3. Создаёт рабочую директорию `data/`, а внутри неё:
|
||||
- `jwt.txt` для сохранённого JWT;
|
||||
- `employees.json` для локального индекса сотрудников;
|
||||
- `keys/` для ключей Data Protection;
|
||||
- `Plugins/` для загружаемых плагинов.
|
||||
4. Подключает Sentry, Swagger, Prometheus, rate limiter, API key auth и OpenID Connect через `Microsoft.Identity.Web`.
|
||||
5. Загружает все плагины `*.plugin.dll` из `data/Plugins` в отдельном `AssemblyLoadContext`.
|
||||
6. Инициализирует Quartz-задачи:
|
||||
- обновление JWT;
|
||||
- обновление локального списка сотрудников;
|
||||
- цепочку, при которой после успешного обновления JWT сразу запускается обновление сотрудников.
|
||||
|
||||
После запуска приложение выбирает источник JWT в таком порядке:
|
||||
|
||||
1. Если задан `TOKEN`, используется он, а фоновое обновление JWT отключается.
|
||||
2. Иначе, если существует `data/jwt.txt` и токен считается ещё пригодным, используется он.
|
||||
3. Иначе приложение триггерит `UpdateJwtJob`, который запрашивает новый JWT у внешнего auth-сервиса.
|
||||
|
||||
Когда JWT уже есть, приложение использует его для запросов к `Modeus`.
|
||||
|
||||
## Основные возможности
|
||||
|
||||
### 1. Проксирование Modeus API
|
||||
|
||||
Контроллер [ProxyController](/home/serega404/Рабочий стол/SfeduSchedule/SfeduSchedule/Controllers/ProxyController.cs) пробрасывает запросы в `Modeus`:
|
||||
|
||||
- `POST /api/proxy/events/search` — получить расписание;
|
||||
- `POST /api/proxy/rooms/search` — искать аудитории.
|
||||
|
||||
Если `Modeus` отвечает ошибкой, сервис возвращает соответствующий HTTP-статус и текст с префиксом `Proxied Modeus:`.
|
||||
|
||||
### 2. Работа с расписанием
|
||||
|
||||
Контроллер [ScheduleController](/home/serega404/Рабочий стол/SfeduSchedule/SfeduSchedule/Controllers/ScheduleController.cs) предоставляет:
|
||||
|
||||
- `GET /api/schedule/getguid?fullname=...` — получить GUID по полному имени, защищено `X-Api-Key`;
|
||||
- `GET /api/schedule/searchemployee?fullname=...` — искать сотрудников по локальному индексу;
|
||||
- `GET /api/schedule/ics?attendeePersonId=...` — получить `ICS` для одного пользователя;
|
||||
- `POST /api/schedule/ics` — получить `ICS` по кастомному `ModeusScheduleRequest`.
|
||||
|
||||
Генерация `ICS` происходит в [ModeusService](/home/serega404/Рабочий стол/SfeduSchedule/SfeduSchedule/Services/ModeusService.cs): сервис получает JSON расписания из `Modeus`, вытаскивает аудитории, преподавателей, краткие названия дисциплин и формирует календарь через `Ical.Net`.
|
||||
|
||||
Диапазон у `GET /api/schedule/ics` зашит в коде: от текущего момента минус 7 дней до плюс 1 месяца.
|
||||
|
||||
### 3. GUID через Microsoft-вход
|
||||
|
||||
Контроллер [SfeduController](/home/serega404/Рабочий стол/SfeduSchedule/SfeduSchedule/Controllers/SfeduController.cs) отдаёт:
|
||||
|
||||
- `GET /api/sfedu/guid`
|
||||
|
||||
Маршрут защищён схемой `OpenIdConnect`. После логина приложение берёт claim `name`, ищет GUID через `Modeus` и:
|
||||
|
||||
- либо возвращает GUID текстом;
|
||||
- либо делает redirect на `redirectUri?guid=...`, если передан параметр `redirectUri`.
|
||||
|
||||
Для этого сценария должны быть корректно настроены переменные `AzureAd:*`.
|
||||
|
||||
### 4. Локальный индекс сотрудников
|
||||
|
||||
[ModeusEmployeeService](/home/serega404/Рабочий стол/SfeduSchedule/SfeduSchedule/Services/ModeusEmployeeService.cs) хранит в памяти словарь сотрудников и их должностей, а также сохраняет его в `data/employees.json`.
|
||||
|
||||
Обновление выполняет Quartz job [UpdateEmployeesJob](/home/serega404/Рабочий стол/SfeduSchedule/SfeduSchedule/Jobs/UpdateEmployeesJob.cs):
|
||||
|
||||
- ходит в `Modeus`;
|
||||
- загружает большой список сотрудников;
|
||||
- группирует записи по ФИО;
|
||||
- сохраняет результат на диск;
|
||||
- повторяет попытки при временных сбоях.
|
||||
|
||||
Если сервис ещё не инициализирован, `GET /api/schedule/searchemployee` возвращает `503`.
|
||||
|
||||
### 5. JWT lifecycle
|
||||
|
||||
Quartz job [UpdateJwtJob](/home/serega404/Рабочий стол/SfeduSchedule/SfeduSchedule/Jobs/UpdateJWTJob.cs):
|
||||
|
||||
- идёт во внешний auth-сервис по `AUTH_URL`;
|
||||
- опционально передаёт `X-API-Key`, если задан `AUTH_API_KEY`;
|
||||
- ожидает JSON вида `{ "jwt": "..." }`;
|
||||
- записывает JWT в runtime-конфигурацию, в `ModeusHttpClient` и в `data/jwt.txt`.
|
||||
|
||||
Если `TOKEN` задан вручную, job не получает триггер по cron и приложение использует этот токен как основной.
|
||||
|
||||
### 6. Метрики и ограничения доступа
|
||||
|
||||
Приложение публикует метрики `prometheus-net` на `/metrics`.
|
||||
|
||||
Для `/metrics` добавлен middleware [LocalNetworksOnlyMiddleware](/home/serega404/Рабочий стол/SfeduSchedule/SfeduSchedule/Middleware/LocalNetworksOnlyMiddleware.cs):
|
||||
|
||||
- разрешён только `GET`;
|
||||
- доступ открыт только из loopback, RFC1918 private networks, link-local и local IPv6 диапазонов;
|
||||
- для остальных адресов возвращается `403`.
|
||||
|
||||
### 7. Ограничение частоты запросов
|
||||
|
||||
Для части API включён rate limit с фиксированным окном:
|
||||
|
||||
- по умолчанию `40` запросов;
|
||||
- за окно `10` секунд;
|
||||
- ключом клиента считается первый IP из `X-Forwarded-For`, либо `RemoteIpAddress`.
|
||||
|
||||
При превышении лимита приложение отвечает `429` и добавляет заголовок `Retry-After`.
|
||||
|
||||
`GET /api/schedule/getguid` специально исключён из rate limiting.
|
||||
|
||||
### 8. Плагины
|
||||
|
||||
На старте хост ищет все `*.plugin.dll` внутри `data/Plugins` и загружает найденные сборки как плагины. Плагин должен реализовывать интерфейс [IPlugin](/home/serega404/Рабочий стол/SfeduSchedule/ModeusSchedule.Abstractions/IPlugin.cs), чтобы:
|
||||
|
||||
- зарегистрировать свои сервисы в DI через `ConfigureServices`;
|
||||
- добавить minimal API маршруты через `MapEndpoints`.
|
||||
|
||||
MVC-контроллеры из плагинов тоже подключаются автоматически через `ApplicationPart`.
|
||||
|
||||
В репозитории есть пример в [SfeduSchedule.Plugin.Sample/Plugin.cs](/home/serega404/Рабочий стол/SfeduSchedule/SfeduSchedule.Plugin.Sample/Plugin.cs).
|
||||
|
||||
## API endpoints
|
||||
|
||||
### Публичные и служебные
|
||||
|
||||
- `GET /` — статическая стартовая страница из `wwwroot/index.html`
|
||||
- `GET /api/docs` — Swagger UI
|
||||
- `GET /api/docs/{documentName}/swagger.json` — OpenAPI JSON
|
||||
- `GET /metrics` — Prometheus metrics, только из локальных/приватных сетей
|
||||
|
||||
### Schedule API
|
||||
|
||||
- `GET /api/schedule/getguid?fullname=...`
|
||||
Требует `X-Api-Key`
|
||||
- `GET /api/schedule/searchemployee?fullname=...`
|
||||
- `GET /api/schedule/ics?attendeePersonId=...`
|
||||
- `POST /api/schedule/ics`
|
||||
|
||||
### Proxy API
|
||||
|
||||
- `POST /api/proxy/events/search`
|
||||
- `POST /api/proxy/rooms/search`
|
||||
|
||||
### Sfedu API
|
||||
|
||||
- `GET /api/sfedu/guid`
|
||||
Требует вход через Microsoft / OpenID Connect
|
||||
|
||||
## Переменные окружения
|
||||
|
||||
Ниже перечислены переменные, которые реально используются кодом или docker-compose файлами проекта.
|
||||
|
||||
### Ключевые переменные приложения
|
||||
|
||||
|
||||
| Переменная | Обязательность | Значение по умолчанию | Где используется | Назначение |
|
||||
| ------------------------- | --------------------------------------- | ------------------------------------------ | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `MODEUS_URL` | Нет | `https://sfedu.modeus.org/` | код | Базовый URL`Modeus` |
|
||||
| `TOKEN` | Условно обязательна | нет | код | Готовый JWT для`Modeus`; если не задан, приложение попытается получить токен через `AUTH_URL` |
|
||||
| `AUTH_URL` | Условно обязательна | `http://msauth:8080/auth/ms` | код | URL сервиса, который возвращает новый JWT |
|
||||
| `AUTH_API_KEY` | Нет | пусто | код | API key для вызова`AUTH_URL`, передаётся как `X-API-Key` |
|
||||
| `API_KEY` | Условно обязательна | нет | код | Ключ для маршрутов, защищённых схемой`ApiKey` |
|
||||
| `UPDATE_JWT_CRON` | Нет | `0 0 4 ? * *` | код | Cron для фонового обновления JWT |
|
||||
| `UPDATE_EMPLOYEES_CRON` | Нет | `0 0 6 ? * *` | код | Cron для фонового обновления индекса сотрудников |
|
||||
| `PERMIT_LIMIT` | Нет | `40` | код | Лимит запросов в окне rate limiter |
|
||||
| `TIME_LIMIT` | Нет | `10` | код | Длина окна rate limiter в секундах |
|
||||
| `TZ` | Нет | `Europe/Moscow` | код | Таймзона для запросов к`Modeus` и генерации `ICS` |
|
||||
| `Sentry:Dsn` | Нет | пустая строка | код | DSN для Sentry |
|
||||
|
||||
### Переменные для Microsoft / Azure AD
|
||||
|
||||
Эти значения напрямую читает `Microsoft.Identity.Web`. В compose-файлах они передаются в контейнер, а в коде используются для `OpenIdConnect`-схемы.
|
||||
|
||||
|
||||
| Переменная | Обязательность | Значение по умолчанию | Назначение |
|
||||
| ------------------------ | --------------------------------------- | ------------------------------------------ | -------------------------------------- |
|
||||
| `AzureAd:Instance` | Условно обязательна | нет | Базовый URL identity provider |
|
||||
| `AzureAd:TenantId` | Условно обязательна | нет | Tenant ID или tenant domain |
|
||||
| `AzureAd:ClientId` | Условно обязательна | нет | Client ID приложения |
|
||||
| `AzureAd:ClientSecret` | Условно обязательна | нет | Client secret приложения |
|
||||
| `AzureAd:Domain` | Условно обязательна | нет | Azure AD domain |
|
||||
| `AzureAd:CallbackPath` | Условно обязательна | нет | Callback path для OIDC |
|
||||
|
||||
Обязательность у `AzureAd:*` условная: без них приложение может быть полезно как API-прокси, но маршрут `GET /api/sfedu/guid` и связанный login-flow работать корректно не будут.
|
||||
|
||||
### Переменные из docker-compose
|
||||
|
||||
|
||||
| Переменная | Обязательность | Значение по умолчанию | Назначение |
|
||||
| --------------------------------------- | ------------------------------ | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `ASPNETCORE_FORWARDEDHEADERS_ENABLED` | Нет | нет | В compose выставляется в`true`, чтобы приложение корректнее работало за reverse proxy |
|
||||
|
||||
### Что значит "условно обязательна"
|
||||
|
||||
`Условно обязательна` означает, что переменная нужна не всегда, а только для конкретного сценария:
|
||||
|
||||
- нужен `TOKEN` или рабочий `AUTH_URL`, чтобы ходить в `Modeus`;
|
||||
- нужен `API_KEY`, если вы хотите использовать `GET /api/schedule/getguid`;
|
||||
- нужны `AzureAd:*`, если используется `GET /api/sfedu/guid`;
|
||||
- нужен `Sentry:Dsn`, если хотите отправлять ошибки и трейсы в Sentry.
|
||||
|
||||
## Пример `.env`
|
||||
|
||||
Для запуска через `docker compose` можно использовать примерно такой `.env`:
|
||||
|
||||
```env
|
||||
API_KEY=change-me
|
||||
|
||||
AUTH_URL=http://msauth:8080/auth/ms
|
||||
AUTH_API_KEY=
|
||||
|
||||
MODEUS_URL=https://sfedu.modeus.org/
|
||||
TZ=Europe/Moscow
|
||||
|
||||
UPDATE_JWT_CRON=0 0 4 ? * *
|
||||
UPDATE_EMPLOYEES_CRON=0 0 6 ? * *
|
||||
|
||||
PERMIT_LIMIT=40
|
||||
TIME_LIMIT=10
|
||||
|
||||
TG_CHAT_ID=
|
||||
TG_TOKEN=
|
||||
|
||||
SENTRY_DSN=
|
||||
|
||||
AzureAd:Instance=https://login.microsoftonline.com/
|
||||
AzureAd:TenantId=sfedu.ru
|
||||
AzureAd:ClientId=
|
||||
AzureAd:ClientSecret=
|
||||
AzureAd:Domain=sfedu.onmicrosoft.com
|
||||
AzureAd:CallbackPath=/signin-oidc
|
||||
```
|
||||
|
||||
Если вы передаёте переменные именно как environment variables вне `docker compose`, для иерархических .NET-ключей чаще используют форму с двойным подчёркиванием:
|
||||
|
||||
```env
|
||||
AzureAd__Instance=https://login.microsoftonline.com/
|
||||
Sentry__Dsn=
|
||||
```
|
||||
|
||||
## Локальный запуск
|
||||
|
||||
### Требования
|
||||
|
||||
- `.NET SDK 10`
|
||||
- доступ к `Modeus`
|
||||
- либо рабочий `TOKEN`, либо доступный `AUTH_URL`
|
||||
|
||||
### Запуск из исходников
|
||||
|
||||
```bash
|
||||
dotnet restore SfeduSchedule.sln
|
||||
dotnet run --project SfeduSchedule/SfeduSchedule.csproj
|
||||
```
|
||||
|
||||
Профили локального запуска прописаны в [launchSettings.json](/home/serega404/Рабочий стол/SfeduSchedule/SfeduSchedule/Properties/launchSettings.json):
|
||||
|
||||
- `http://localhost:5087`
|
||||
- `https://localhost:7146`
|
||||
|
||||
## Запуск в Docker
|
||||
|
||||
### Production compose
|
||||
|
||||
Файл [docker-compose-prod.yml](/home/serega404/Рабочий стол/SfeduSchedule/docker-compose-prod.yml):
|
||||
|
||||
- поднимает контейнер `SfeduSchedule`;
|
||||
- публикует порт `8088 -> 8080`;
|
||||
- использует именованный volume `data:/app/data`;
|
||||
- ожидает готовый образ `git.zetcraft.ru/serega404/sfeduschedule:main`.
|
||||
|
||||
Пример запуска:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose-prod.yml up -d
|
||||
```
|
||||
|
||||
### Test compose
|
||||
|
||||
Файл [docker-compose-test.yml](/home/serega404/Рабочий стол/SfeduSchedule/docker-compose-test.yml) предназначен для локальной сборки и монтирует `./data:/app/data`.
|
||||
|
||||
Пример запуска:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose-test.yml up --build
|
||||
```
|
||||
|
||||
Важно: в текущем виде `docker-compose-test.yml` ссылается на `dockerfile: Dockerfile` при `context: ./SfeduSchedule`, тогда как `Dockerfile` лежит в корне репозитория. Перед использованием этот compose-файл, вероятно, нужно скорректировать под фактическую структуру проекта.
|
||||
|
||||
## Структура данных в `data/`
|
||||
|
||||
Во время работы приложение заполняет:
|
||||
|
||||
- `data/jwt.txt` — первая строка JWT, вторая строка timestamp его сохранения;
|
||||
- `data/employees.json` — локальный индекс сотрудников;
|
||||
- `data/keys/` — ключи Data Protection;
|
||||
- `data/Plugins/` — каталог для плагинов.
|
||||
|
||||
Эту директорию стоит сохранять между рестартами контейнера.
|
||||
|
||||
## Безопасность и эксплуатационные детали
|
||||
|
||||
- `API_KEY` можно передавать в заголовке `X-Api-Key`, а также в query string как `api_key`.
|
||||
- Для `X-Api-Key` используется сравнение в постоянное время.
|
||||
- Приложение обрабатывает `X-Forwarded-For`, `X-Forwarded-Proto` и `X-Forwarded-Host`.
|
||||
- В ответ на каждый HTTP-запрос добавляется `X-Correlation-ID`.
|
||||
- Swagger опубликован по адресу `/api/docs`, а не по стандартному `/swagger`.
|
||||
- Ключи Data Protection сохраняются на диск, поэтому cookie и служебные ключи не меняются на каждый рестарт.
|
||||
|
||||
## Плагины
|
||||
|
||||
Чтобы подключить плагин:
|
||||
|
||||
1. Соберите библиотеку с именем вида `*.plugin.dll`.
|
||||
2. Убедитесь, что она реализует `ModeusSchedule.Abstractions.IPlugin`.
|
||||
3. Положите сборку и её зависимости в `data/Plugins/<имя-плагина>/`.
|
||||
4. Перезапустите приложение.
|
||||
|
||||
Хост:
|
||||
|
||||
- создаст для плагина отдельный `AssemblyLoadContext`;
|
||||
- вызовет `ConfigureServices`;
|
||||
- подключит MVC-контроллеры из сборки;
|
||||
- вызовет `MapEndpoints` после сборки приложения.
|
||||
|
||||
## Что стоит учитывать
|
||||
|
||||
- `GET /api/schedule/searchemployee` зависит от успешной инициализации локального индекса сотрудников.
|
||||
- Если не настроен `AUTH_URL` и не задан `TOKEN`, приложение не сможет полноценно работать с `Modeus`.
|
||||
- Проверка срока жизни JWT в `jwt.txt` основана на времени сохранения файла, а не на `exp` claim токена.
|
||||
- В коде включены `Sentry` tracing и profiling с sample rate `1.0`, это может быть важно для нагрузки и объёма телеметрии.
|
||||
|
||||
## Проверка после запуска
|
||||
|
||||
Минимальный smoke check:
|
||||
|
||||
```bash
|
||||
curl http://localhost:5087/
|
||||
curl http://localhost:5087/api/docs
|
||||
curl http://localhost:5087/metrics
|
||||
```
|
||||
|
||||
Для защищённого эндпоинта:
|
||||
|
||||
```bash
|
||||
curl -H "X-Api-Key: $API_KEY" \
|
||||
"http://localhost:5087/api/schedule/getguid?fullname=Иванов%20Иван%20Иванович"
|
||||
```
|
||||
|
||||
@@ -11,10 +11,6 @@ public static class AppConsts
|
||||
public const string ModeusUrlEnv = "MODEUS_URL";
|
||||
public const string ModeusDefaultUrl = "https://sfedu.modeus.org/";
|
||||
|
||||
// Telegram
|
||||
public const string TgChatIdEnv = "TG_CHAT_ID";
|
||||
public const string TgTokenEnv = "TG_TOKEN";
|
||||
|
||||
// RateLimiter
|
||||
public const string PermitLimitEnv = "PERMIT_LIMIT";
|
||||
public const string TimeLimitEnv = "TIME_LIMIT";
|
||||
|
||||
@@ -3,7 +3,6 @@ using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using ModeusSchedule.Abstractions;
|
||||
using ModeusSchedule.Abstractions.DTO;
|
||||
using ModeusSchedule.Abstractions.DTO.Requests;
|
||||
using SfeduSchedule.Services;
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using ModeusSchedule.Abstractions.DTO;
|
||||
using ModeusSchedule.Abstractions.DTO.Requests;
|
||||
using SfeduSchedule.DTO.Responses;
|
||||
using SfeduSchedule.Services;
|
||||
@@ -47,7 +45,7 @@ public class ScheduleController(ModeusService modeusService, ModeusEmployeeServi
|
||||
/// <response code="503">Сервис сотрудников не инициализирован</response>
|
||||
[HttpGet]
|
||||
[Route("searchemployee")]
|
||||
public async Task<IActionResult> SearchEmployees([Required][MinLength(1)] string fullname)
|
||||
public async Task<IActionResult> SearchEmployees(string? fullname)
|
||||
{
|
||||
if (!modeusEmployeeService.IsInitialized())
|
||||
return StatusCode(503, "Сервис сотрудников не инициализирован, попробуйте позже.");
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using ModeusSchedule.Abstractions;
|
||||
using Quartz;
|
||||
using SfeduSchedule.Services;
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.Mvc.ApplicationParts;
|
||||
using Microsoft.Identity.Web;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using ModeusSchedule.Abstractions;
|
||||
using Prometheus;
|
||||
using Quartz;
|
||||
using Quartz.Listener;
|
||||
@@ -19,7 +18,6 @@ using SfeduSchedule.Jobs;
|
||||
using SfeduSchedule.Logging;
|
||||
using SfeduSchedule.Middleware;
|
||||
using SfeduSchedule.Services;
|
||||
using X.Extensions.Logging.Telegram.Extensions;
|
||||
using IPNetwork = Microsoft.AspNetCore.HttpOverrides.IPNetwork;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
@@ -60,26 +58,6 @@ builder.Logging.AddConsole(options => options.FormatterName = "CustomConsoleForm
|
||||
.AddConsoleFormatter<ConsoleFormatter, ConsoleFormatterOptions>();
|
||||
|
||||
builder.Logging.AddFilter("Quartz", LogLevel.Warning);
|
||||
if (!string.IsNullOrEmpty(configuration[AppConsts.TgChatIdEnv]) && !string.IsNullOrEmpty(configuration[AppConsts.TgTokenEnv]))
|
||||
builder.Logging.AddTelegram(options =>
|
||||
{
|
||||
options.FormatterConfiguration = new X.Extensions.Logging.Telegram.Base.Configuration.FormatterConfiguration
|
||||
{
|
||||
IncludeException = true,
|
||||
IncludeProperties = true,
|
||||
};
|
||||
options.ChatId = configuration[AppConsts.TgChatIdEnv]!;
|
||||
options.AccessToken = configuration[AppConsts.TgTokenEnv]!;
|
||||
options.FormatterConfiguration.UseEmoji = true;
|
||||
options.FormatterConfiguration.ReadableApplicationName = "Modeus Schedule Proxy";
|
||||
options.LogLevel = new Dictionary<string, LogLevel>
|
||||
{
|
||||
{ "Default", LogLevel.Error },
|
||||
{ "SfeduSchedule.Jobs.UpdateJwtJob", LogLevel.Information },
|
||||
{ "Program", LogLevel.Information },
|
||||
{ "Quartz", LogLevel.Warning }
|
||||
};
|
||||
});
|
||||
#endregion
|
||||
|
||||
builder.WebHost.UseSentry(options =>
|
||||
@@ -319,8 +297,14 @@ if (refreshJwt)
|
||||
else
|
||||
await scheduler.TriggerJob(new JobKey(nameof(UpdateEmployeesJob)));
|
||||
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
app.UseSwagger(options =>
|
||||
{
|
||||
options.RouteTemplate = "api/docs/{documentName}/swagger.json";
|
||||
});
|
||||
app.UseSwaggerUI(options =>
|
||||
{
|
||||
options.RoutePrefix = "api/docs";
|
||||
});
|
||||
|
||||
app.UseStaticFiles();
|
||||
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
using Quartz;
|
||||
using SfeduSchedule.Jobs;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SfeduSchedule.Services;
|
||||
|
||||
public class ModeusEmployeeService(ISchedulerFactory schedulerFactory)
|
||||
public class ModeusEmployeeService
|
||||
: IHostedService
|
||||
{
|
||||
private Dictionary<string, (string, List<string>)> _employees = [];
|
||||
private Dictionary<string, (string, List<string>)> _employees = []; // ФИО (ИД, Список должностей)
|
||||
private Task? _backgroundTask;
|
||||
private CancellationTokenSource? _cts;
|
||||
private readonly string _employeesFilePath = Path.Combine(Path.Combine(AppContext.BaseDirectory, AppConsts.DataFolderName), AppConsts.EmployeesFileName);
|
||||
|
||||
public async Task<Dictionary<string, (string, List<string>)>> GetEmployees(string fullname, int size = 10)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fullname))
|
||||
return _employees
|
||||
.OrderBy(e => e.Key)
|
||||
.Take(size)
|
||||
.ToDictionary(e => e.Key, e => e.Value);
|
||||
|
||||
return _employees
|
||||
.Where(e => e.Key.Contains(fullname, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(e => e.Key)
|
||||
.Take(size)
|
||||
.ToDictionary(e => e.Key, e => e.Value);
|
||||
}
|
||||
|
||||
|
||||
public bool IsInitialized()
|
||||
{
|
||||
return _employees.Count > 0;
|
||||
@@ -64,7 +70,7 @@ public class ModeusEmployeeService(ISchedulerFactory schedulerFactory)
|
||||
|
||||
private async Task LoadEmployeesFromDisk()
|
||||
{
|
||||
|
||||
|
||||
if (File.Exists(_employeesFilePath))
|
||||
{
|
||||
var json = await File.ReadAllTextAsync(_employeesFilePath);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using ModeusSchedule.Abstractions;
|
||||
using ModeusSchedule.Abstractions.DTO;
|
||||
|
||||
@@ -8,7 +8,6 @@ using ModeusSchedule.Abstractions;
|
||||
using ModeusSchedule.Abstractions.DTO;
|
||||
using ModeusSchedule.Abstractions.DTO.Requests;
|
||||
using SfeduSchedule.DTO.Requests;
|
||||
using SfeduSchedule.DTO.Responses;
|
||||
using SfeduSchedule.Logging;
|
||||
|
||||
namespace SfeduSchedule.Services;
|
||||
@@ -234,21 +233,22 @@ public class ModeusService(
|
||||
}
|
||||
}
|
||||
|
||||
var position = (e.GroupName ?? string.Empty).Trim();
|
||||
|
||||
static string FormatDateRange(string? dateIn, string? dateOut)
|
||||
{
|
||||
var start = (dateIn ?? string.Empty).Trim();
|
||||
if (string.IsNullOrEmpty(start)) start = "?";
|
||||
|
||||
var end = (dateOut ?? string.Empty).Trim();
|
||||
if (string.IsNullOrEmpty(end)) end = "по наст.вр.";
|
||||
|
||||
return $"{start}–{end}";
|
||||
}
|
||||
|
||||
var dateRange = FormatDateRange(e.DateIn, e.DateOut);
|
||||
var positionWithDates = string.IsNullOrEmpty(position) ? $"({dateRange})" : $"{position} ({dateRange})";
|
||||
// Отключено так как информация нигде не используется
|
||||
// var position = (e.GroupName ?? string.Empty).Trim();
|
||||
//
|
||||
// static string FormatDateRange(string? dateIn, string? dateOut)
|
||||
// {
|
||||
// var start = (dateIn ?? string.Empty).Trim();
|
||||
// if (string.IsNullOrEmpty(start)) start = "?";
|
||||
//
|
||||
// var end = (dateOut ?? string.Empty).Trim();
|
||||
// if (string.IsNullOrEmpty(end)) end = "по наст.вр.";
|
||||
//
|
||||
// return $"{start}–{end}";
|
||||
// }
|
||||
//
|
||||
// var dateRange = FormatDateRange(e.DateIn, e.DateOut);
|
||||
// var positionWithDates = string.IsNullOrEmpty(position) ? $"({dateRange})" : $"{position} ({dateRange})"; // Место работы с датой
|
||||
|
||||
if (!grouped.TryGetValue(fullName, out var entry))
|
||||
{
|
||||
@@ -261,11 +261,11 @@ public class ModeusService(
|
||||
entry.PersonId = personId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!entry.Positions.Contains(positionWithDates))
|
||||
{
|
||||
entry.Positions.Add(positionWithDates);
|
||||
}
|
||||
|
||||
// if (!entry.Positions.Contains(positionWithDates))
|
||||
// {
|
||||
// entry.Positions.Add(positionWithDates);
|
||||
// }
|
||||
|
||||
grouped[fullName] = entry;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<PackageReference Include="Sentry.AspNetCore" Version="6.1.0"/>
|
||||
<PackageReference Include="Sentry.Profiling" Version="6.1.0"/>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6"/>
|
||||
<PackageReference Include="X.Extensions.Logging.Telegram" Version="2.0.2"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -11,8 +11,6 @@ services:
|
||||
- AzureAd:ClientSecret=
|
||||
- AzureAd:Domain=sfedu.onmicrosoft.com
|
||||
- AzureAd:CallbackPath=/signin-oidc
|
||||
- TG_CHAT_ID=${TG_CHAT_ID}
|
||||
- TG_TOKEN=${TG_TOKEN}
|
||||
- API_KEY=${API_KEY}
|
||||
# - TOKEN=${TOKEN}
|
||||
- AUTH_URL=${AUTH_URL}
|
||||
|
||||
@@ -11,8 +11,6 @@ services:
|
||||
- AzureAd:ClientSecret=
|
||||
- AzureAd:Domain=sfedu.onmicrosoft.com
|
||||
- AzureAd:CallbackPath=/signin-oidc
|
||||
- TG_CHAT_ID=${TG_CHAT_ID}
|
||||
- TG_TOKEN=${TG_TOKEN}
|
||||
- API_KEY=${API_KEY}
|
||||
# - TOKEN=${TOKEN}
|
||||
- AUTH_URL=${AUTH_URL}
|
||||
|
||||
Reference in New Issue
Block a user