Локальное окружение требовало двух терминалов, а без запущенного auth-сервиса форма отдаёт 500 от vite-прокси — это легко принять за баг приложения. Теперь concurrently стартует оба процесса с метками [web]/[auth]; только фронтенд — npm run dev:web. Заодно поправлен README сервиса: GRAPH_SCOPE больше не нужно задавать руками, дефолт уже указывает на App ID URI api.tnsu.kz. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
hr-appeals-auth
Сервис получает app-only токен Azure AD (JWT client
assertion) и используется для авторизованных запросов к внутреннему API
api.tnsu.kz. Сам токен наружу не отдаётся — из HTTP-эндпоинтов сервиса
доступны только /healthz и /diag/token (последний подтверждает, что
токен получен, но не возвращает его значение). Детали получения токена — в
graphAuth.ts.
Переменные окружения
| Переменная | Назначение |
|---|---|
GRAPH_TENANT_ID |
Tenant ID Azure AD |
GRAPH_CLIENT_ID |
Client ID App Registration, к которому привязан сертификат SPFxDeploy |
GRAPH_CERT_PATH |
Путь к SPFxDeploy_cert.pem внутри контейнера (см. docker-compose.yml в корне репозитория) |
GRAPH_KEY_PATH |
Путь к SPFxDeploy_key.pem внутри контейнера |
GRAPH_SCOPE |
OAuth-scope запрашиваемого токена. По умолчанию — App ID URI самого api.tnsu.kz (api://9232d9e2-.../.default), задавать не нужно. Токен для https://graph.microsoft.com/.default тут не подойдёт: api.tnsu.kz проверяет aud входящего JWT и отвечает 401 |
PORT |
Порт HTTP-сервера сервиса, по умолчанию 3000 (локально — 3100, см. .env.example) |
Локальный запуск
Из корня репозитория npm run dev поднимает сервис вместе с фронтендом. Отдельно:
cp .env.example .env # подставить свои пути к SPFxDeploy_cert.pem / _key.pem
npm run dev # tsc + запуск с чтением .env (--env-file, Node 20+)
Проверить, что авторизация работает: curl http://localhost:3100/diag/token →
{"ok":true}. Если вернулась ошибка — в ней будет ответ Azure AD целиком.
Как обратиться к api.tnsu.kz с авторизацией
Токен запрашивается через getAppOnlyToken() и передаётся в заголовке
Authorization: Bearer <token>. Функция сама кэширует токен до истечения,
повторно вызывать её на каждый запрос не накладно — за сеть она сходит
только когда кэш устарел.
import { getAppOnlyToken } from './graphAuth';
async function callApiTnsu(path: string, init: RequestInit = {}) {
const token = await getAppOnlyToken();
const response = await fetch(`https://api.tnsu.kz${path}`, {
...init,
headers: {
...init.headers,
Authorization: `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error(`api.tnsu.kz ответил ${response.status}: ${await response.text()}`);
}
return response.json();
}
Перед тем как писать реальные вызовы, убедитесь, что обмен сертификата на токен вообще работает — на запущенном сервисе:
curl http://localhost:3000/diag/token
# {"ok":true} — сертификат подошёл, токен получен и закэширован
# {"ok":false,"error":"..."} — см. текст ошибки: неверный tenant/client id,
# истёкший сертификат, неверный scope и т.п.
Если /diag/token отвечает ok:true, но реальный вызов api.tnsu.kz
падает с 401/403 — вероятная причина в GRAPH_SCOPE: токен получен, но
выпущен не для этого API (Azure AD не проверяет аудиторию на этапе выдачи
токена — она проверяется уже на стороне api.tnsu.kz).