From f60f4903d4bafd281fdb22666c2faab423b4f9b3 Mon Sep 17 00:00:00 2001 From: "gorbenko.pavel" Date: Thu, 24 Sep 2026 17:58:02 +0500 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D1=81=D1=82=D1=83=D0=BF=20?= =?UTF-8?q?=D0=BA=20=C2=AB=D0=9E=D1=82=D1=87=D1=91=D1=82=D0=B0=D0=BC=C2=BB?= =?UTF-8?q?=20=D0=BF=D0=BE=20=D0=B3=D1=80=D1=83=D0=BF=D0=BF=D0=B5=20ShareP?= =?UTF-8?q?oint=20=C2=AB=D0=9E=D1=82=D1=87=D0=B5=D1=82=D1=8B=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D0=B7=D0=B0=D1=8F=D0=B2=D0=BA=D0=B0=D0=BC=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Вместо точечного списка почт — членство в группе (Id 199); прежние четыре почты остаются как администраторы с доступом независимо от группы. Co-Authored-By: Claude Opus 5.5 --- .../components/RequestPortal.tsx | 27 +++++++--- .../shared/reports/reportsAccess.ts | 54 ++++++++++++++----- 2 files changed, 62 insertions(+), 19 deletions(-) diff --git a/src/webparts/requestPortal/components/RequestPortal.tsx b/src/webparts/requestPortal/components/RequestPortal.tsx index bb098af..54995fc 100644 --- a/src/webparts/requestPortal/components/RequestPortal.tsx +++ b/src/webparts/requestPortal/components/RequestPortal.tsx @@ -73,18 +73,31 @@ const RequestPortal: React.FC = ({ hasTeamsContext, context const [mobileNavOpen, setMobileNavOpen] = React.useState(false); const [navCounts, setNavCounts] = React.useState(undefined); - // Раздел «Отчёты» — точечный список почт, пока в SharePoint нет группы HR - // (см. reportsAccess.ts). Пересчитывается на каждый рендер намеренно: сам - // hasReportsAccess — синхронная проверка без сети, а input (effectiveUserEmail) - // меняется только по подмене личности в property pane, что и так вызывает - // ререндер всей веб-части. - const canSeeReports = hasReportsAccess(context); + // Раздел «Отчёты» — группа «Отчеты по заявкам» + администраторы (см. + // reportsAccess.ts). undefined — ответ ещё не пришёл. Перезапрашиваем при + // смене подменённой личности. + const [canSeeReports, setCanSeeReports] = React.useState(undefined); + React.useEffect(() => { + let cancelled = false; + setCanSeeReports(undefined); + hasReportsAccess(context) + .then((v) => { + if (!cancelled) setCanSeeReports(v); + }) + .catch(() => { + if (!cancelled) setCanSeeReports(false); + }); + return () => { + cancelled = true; + }; + }, [context, impersonatedEmail]); // Прямой переход по ссылке (?rpnav=reports) в обход бокового меню — тот же // раздел, что скрыт из навигации, должен быть недоступен и так: без этого // редиректа он открылся бы у любого, кто угадает или сохранит такую ссылку. + // Пока ответ не пришёл — не редиректим, иначе участника группы выкинет. React.useEffect(() => { - if (route.nav === 'reports' && !canSeeReports) { + if (route.nav === 'reports' && canSeeReports === false) { navigate({ mode: 'submit', nav: 'new' }, { replace: true }); } }, [route.nav, canSeeReports]); diff --git a/src/webparts/requestPortal/shared/reports/reportsAccess.ts b/src/webparts/requestPortal/shared/reports/reportsAccess.ts index f656271..f6a23d5 100644 --- a/src/webparts/requestPortal/shared/reports/reportsAccess.ts +++ b/src/webparts/requestPortal/shared/reports/reportsAccess.ts @@ -1,34 +1,64 @@ import { WebPartContext } from '@microsoft/sp-webpart-base'; -import { effectiveUserEmail } from '../impersonation'; +import '@pnp/sp/webs'; +import '@pnp/sp/site-users/web'; +import '@pnp/sp/site-groups/web'; +import { getSP } from '../pnp/pnpjs'; +import { effectiveUserEmail, getImpersonatedEmail } from '../impersonation'; /** - * Кто видит раздел «Отчёты» в боковом меню. Пока это фиксированный список - * почт — группы HR в SharePoint ещё нет (её заведут отдельно); появится — - * список ниже заменится на реальную проверку членства (см. TODO). + * SharePoint-группа «Отчеты по заявкам» на сайте данных (/sites/requests) — + * её участники видят раздел «Отчёты». Id, а не Title: Title можно + * переименовать в SharePoint без последствий для кода. + */ +const REPORTS_GROUP_ID = 199; + +/** + * Администраторы портала — видят раздел независимо от состава группы. * * Email — из справочника сотрудников (api.tnsu.kz/Employee/All), а не из * Title сайта: у части учёток встречаются устаревшие site user записи со * старым email (пример — Тришина: m.trishina@tnsu.kz как устаревший алиас * рядом с текущим trishina.mariya@tnsu.kz). */ -const ALLOWED_EMAILS: ReadonlySet = new Set([ +const ALWAYS_ALLOWED_EMAILS: ReadonlySet = new Set([ 'trishina.mariya@tnsu.kz', // Мария Тришина 'gorodetskiy.timur@tnsu.kz', // Тимур Городецкий 'gorbenko.pavel@tnsu.kz', // Павел Горбенко 'sarniyazova.assilya@tnsu.kz', // Асилья Сарниязова ]); -// TODO: заменить точечный список на членство в SharePoint-группе HR, когда -// её заведут — тогда список выше станет фолбэком для тех троих, кто должен -// видеть раздел независимо от состава группы (администраторы портала). +const cache = new Map>(); + +async function fetchIsGroupMember(email: string): Promise { + // Под подменой личности спрашиваем состав группы, а не currentUser: иначе + // проверялся бы реальный пользователь. Состав группы виден только её + // участникам и владельцам сайта — остальным запрос вернёт ошибку. + if (getImpersonatedEmail()) { + const users = await getSP().web.siteGroups.getById(REPORTS_GROUP_ID).users.select('Email')(); + return users.some((u) => (u.Email || '').trim().toLowerCase() === email); + } + const groups = await getSP().web.currentUser.groups.select('Id')(); + return groups.some((g) => g.Id === REPORTS_GROUP_ID); +} /** * Есть ли у текущего пользователя доступ к разделу «Отчёты». Использует * effectiveUserEmail — то есть учитывает отладочную подмену личности (см. - * impersonation.ts): под подменённым пользователем раздел виден/не виден - * так же, как было бы у него самого. + * impersonation.ts). Ответ кешируется на сеанс по email; ошибка запроса = нет + * доступа и не кешируется, чтобы временный сбой не «залип». */ -export function hasReportsAccess(context: WebPartContext): boolean { +export function hasReportsAccess(context: WebPartContext): Promise { const email = effectiveUserEmail(context).trim().toLowerCase(); - return email.length > 0 && ALLOWED_EMAILS.has(email); + if (!email) return Promise.resolve(false); + if (ALWAYS_ALLOWED_EMAILS.has(email)) return Promise.resolve(true); + + let pending = cache.get(email); + if (!pending) { + pending = fetchIsGroupMember(email).catch(() => { + cache.delete(email); + return false; + }); + cache.set(email, pending); + } + return pending; }