имперсонейшн
This commit is contained in:
@@ -13,10 +13,13 @@ import RequestPortal from './components/RequestPortal';
|
|||||||
import { IRequestPortalProps } from './components/IRequestPortalProps';
|
import { IRequestPortalProps } from './components/IRequestPortalProps';
|
||||||
import { initSP } from './shared/pnp/pnpjs';
|
import { initSP } from './shared/pnp/pnpjs';
|
||||||
import { installErrorLogging } from './shared/diagnostics';
|
import { installErrorLogging } from './shared/diagnostics';
|
||||||
|
import { setImpersonatedEmail } from './shared/impersonation';
|
||||||
import { ErrorOverlay } from './shared/ui';
|
import { ErrorOverlay } from './shared/ui';
|
||||||
|
|
||||||
export interface IRequestPortalWebPartProps {
|
export interface IRequestPortalWebPartProps {
|
||||||
description: string;
|
description: string;
|
||||||
|
/** Отладочная подмена личности — см. shared/impersonation.ts. На бою пусто. */
|
||||||
|
debugUserEmail: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class RequestPortalWebPart extends BaseClientSideWebPart<IRequestPortalWebPartProps> {
|
export default class RequestPortalWebPart extends BaseClientSideWebPart<IRequestPortalWebPartProps> {
|
||||||
@@ -25,6 +28,10 @@ export default class RequestPortalWebPart extends BaseClientSideWebPart<IRequest
|
|||||||
private _environmentMessage: string = '';
|
private _environmentMessage: string = '';
|
||||||
|
|
||||||
public render(): void {
|
public render(): void {
|
||||||
|
// Именно здесь, а не в onInit: свойство меняется в панели без перезапуска
|
||||||
|
// веб-части, и render вызывается на каждое такое изменение.
|
||||||
|
setImpersonatedEmail(this.properties.debugUserEmail);
|
||||||
|
|
||||||
const element: React.ReactElement = React.createElement(
|
const element: React.ReactElement = React.createElement(
|
||||||
ErrorOverlay,
|
ErrorOverlay,
|
||||||
undefined,
|
undefined,
|
||||||
@@ -117,6 +124,11 @@ export default class RequestPortalWebPart extends BaseClientSideWebPart<IRequest
|
|||||||
groupFields: [
|
groupFields: [
|
||||||
PropertyPaneTextField('description', {
|
PropertyPaneTextField('description', {
|
||||||
label: strings.DescriptionFieldLabel
|
label: strings.DescriptionFieldLabel
|
||||||
|
}),
|
||||||
|
PropertyPaneTextField('debugUserEmail', {
|
||||||
|
label: strings.DebugUserEmailFieldLabel,
|
||||||
|
description: strings.DebugUserEmailFieldDescription,
|
||||||
|
placeholder: 'employee@tnsu.kz'
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,5 +93,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Полоса отладочной подмены личности (см. shared/impersonation.ts). Красная и
|
||||||
|
// во всю ширину намеренно: это не подсказка, а предупреждение о том, что портал
|
||||||
|
// показывает не ваши данные — незаметная плашка тут хуже, чем никакой.
|
||||||
|
.impersonationBar {
|
||||||
|
padding: $sp-3 $sp-4;
|
||||||
|
margin-bottom: $sp-4;
|
||||||
|
border: 1px solid $red-500;
|
||||||
|
border-radius: $r-md;
|
||||||
|
background: rgba($red-500, .08);
|
||||||
|
color: $navy-900;
|
||||||
|
font-size: $fs-sm;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
// Стили конкретных фич живут в features/<name>/<Name>.module.scss.
|
// Стили конкретных фич живут в features/<name>/<Name>.module.scss.
|
||||||
// Здесь — только оболочка (root + раскладка колонок), общая для всех фич.
|
// Здесь — только оболочка (root + раскладка колонок), общая для всех фич.
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
OwnTab,
|
OwnTab,
|
||||||
} from '../shared/routing';
|
} from '../shared/routing';
|
||||||
import { onRequestsChanged, emitRequestsChanged } from '../shared/requestEvents';
|
import { onRequestsChanged, emitRequestsChanged } from '../shared/requestEvents';
|
||||||
|
import { getImpersonatedEmail } from '../shared/impersonation';
|
||||||
import { confirmDiscardFormChanges, markFormClean, markFormDirty } from '../shared/unsavedChanges';
|
import { confirmDiscardFormChanges, markFormClean, markFormDirty } from '../shared/unsavedChanges';
|
||||||
import { SideNav } from './SideNav/SideNav';
|
import { SideNav } from './SideNav/SideNav';
|
||||||
import { Drawer } from './Drawer/Drawer';
|
import { Drawer } from './Drawer/Drawer';
|
||||||
@@ -57,6 +58,11 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ');
|
.join(' ');
|
||||||
|
|
||||||
|
// Читаем при рендере, а не через состояние: значение выставляет веб-часть
|
||||||
|
// (setImpersonatedEmail в render) до создания этого элемента, и меняется оно
|
||||||
|
// только вместе с новым render всей веб-части.
|
||||||
|
const impersonatedEmail = getImpersonatedEmail();
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const [navCollapsed, setNavCollapsed] = React.useState<boolean>(false);
|
const [navCollapsed, setNavCollapsed] = React.useState<boolean>(false);
|
||||||
const [navCounts, setNavCounts] = React.useState<INavCounts | undefined>(undefined);
|
const [navCounts, setNavCounts] = React.useState<INavCounts | undefined>(undefined);
|
||||||
@@ -254,6 +260,15 @@ const RequestPortal: React.FC<IRequestPortalProps> = ({ hasTeamsContext, context
|
|||||||
<section className={rootClasses}>
|
<section className={rootClasses}>
|
||||||
<IconSprite />
|
<IconSprite />
|
||||||
<ConfirmHost />
|
<ConfirmHost />
|
||||||
|
{/* Подмену личности легко забыть включённой — тогда портал молча показывал
|
||||||
|
бы чужие заявки и чужую очередь. Полосу поэтому не прячем и не даём
|
||||||
|
закрыть: она исчезает только когда свойство веб-части очищено. */}
|
||||||
|
{impersonatedEmail && (
|
||||||
|
<div className={styles.impersonationBar}>
|
||||||
|
Включена отладочная подмена: портал работает от имени <strong>{impersonatedEmail}</strong>.
|
||||||
|
Очистите поле в свойствах веб-части, чтобы вернуться к своей учётной записи.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className={styles.layout}>
|
<div className={styles.layout}>
|
||||||
<div className={styles.side}>
|
<div className={styles.side}>
|
||||||
<SideNav
|
<SideNav
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import '@pnp/sp/site-users/web';
|
|||||||
import '@pnp/sp/search';
|
import '@pnp/sp/search';
|
||||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
import { getRequestTypes, IRequestType, REQUEST_STATUS } from '../../shared/approval';
|
import { getRequestTypes, IRequestType, REQUEST_STATUS } from '../../shared/approval';
|
||||||
|
import { effectiveUserEmail } from '../../shared/impersonation';
|
||||||
import { IMyRequest } from './models/IMyRequest';
|
import { IMyRequest } from './models/IMyRequest';
|
||||||
import { IMyRequestsTypeDebug } from './models/IMyRequestsDebug';
|
import { IMyRequestsTypeDebug } from './models/IMyRequestsDebug';
|
||||||
|
|
||||||
@@ -53,8 +54,12 @@ const MP_APPROVER = 'TansuSiteApproverOWSUSER';
|
|||||||
// AuthorOWSUSER, статус ни при чём.
|
// AuthorOWSUSER, статус ни при чём.
|
||||||
const MP_STATUS = 'ApprovalStatusOWSCHCS';
|
const MP_STATUS = 'ApprovalStatusOWSCHCS';
|
||||||
|
|
||||||
|
// effectiveUserEmail, а не pageContext напрямую: при отладочной подмене личности
|
||||||
|
// (см. shared/impersonation.ts) «мои заявки» и очереди согласования должны
|
||||||
|
// отбираться по подменённому сотруднику — иначе подмена показывала бы чужой
|
||||||
|
// маршрут в форме, но собственные списки заявок.
|
||||||
export function currentUserEmail(context: WebPartContext): string {
|
export function currentUserEmail(context: WebPartContext): string {
|
||||||
return (context.pageContext.user.email || '').toLowerCase();
|
return effectiveUserEmail(context).toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Типы заявок, у которых approval-route API уже отдал listId/siteUrl (готовы к чтению).
|
// Типы заявок, у которых approval-route API уже отдал listId/siteUrl (готовы к чтению).
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
deleteAttachment,
|
deleteAttachment,
|
||||||
IAttachmentInfo
|
IAttachmentInfo
|
||||||
} from '../../shared/attachments/attachments.service';
|
} from '../../shared/attachments/attachments.service';
|
||||||
|
import { effectiveUserEmail } from '../../shared/impersonation';
|
||||||
import { IRequestFormProps } from '../../models/types';
|
import { IRequestFormProps } from '../../models/types';
|
||||||
import { IFormData, EMPTY_DATA, SetField } from './model';
|
import { IFormData, EMPTY_DATA, SetField } from './model';
|
||||||
import { saveHireRequest, hireApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit';
|
import { saveHireRequest, hireApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit';
|
||||||
@@ -89,7 +90,7 @@ const RecruitingHireForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
|||||||
// редактирование, маршрут читается из уже записанных строк — планирование
|
// редактирование, маршрут читается из уже записанных строк — планирование
|
||||||
// затёрло бы правки, сделанные при первом сохранении.
|
// затёрло бы правки, сделанные при первом сохранении.
|
||||||
const approval = useMemo(() => hireApprovalService(), []);
|
const approval = useMemo(() => hireApprovalService(), []);
|
||||||
const userEmail = context.pageContext.user.email;
|
const userEmail = effectiveUserEmail(context);
|
||||||
// pageUrl формы (requestType.id совпадает с ним по конвенции портала, см.
|
// pageUrl формы (requestType.id совпадает с ним по конвенции портала, см.
|
||||||
// data/requests.ts) → числовой RequestTypeId, нужный approval-route API.
|
// data/requests.ts) → числовой RequestTypeId, нужный approval-route API.
|
||||||
const requestTypeIdState = useRequestTypeId(context, requestType.id);
|
const requestTypeIdState = useRequestTypeId(context, requestType.id);
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
deleteAttachment,
|
deleteAttachment,
|
||||||
IAttachmentInfo
|
IAttachmentInfo
|
||||||
} from '../../shared/attachments/attachments.service';
|
} from '../../shared/attachments/attachments.service';
|
||||||
|
import { effectiveUserEmail } from '../../shared/impersonation';
|
||||||
import { IRequestFormProps } from '../../models/types';
|
import { IRequestFormProps } from '../../models/types';
|
||||||
import { IFormData, EMPTY_DATA, SetField } from './model';
|
import { IFormData, EMPTY_DATA, SetField } from './model';
|
||||||
import { saveSelectionRequest, selectionApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit';
|
import { saveSelectionRequest, selectionApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit';
|
||||||
@@ -86,7 +87,7 @@ const RecruitingSelectionForm: React.FC<IRequestFormProps> = ({ requestType, con
|
|||||||
// У заявки на редактировании маршрут читается из уже записанных строк, чтобы не
|
// У заявки на редактировании маршрут читается из уже записанных строк, чтобы не
|
||||||
// затереть правки инициатора при первом сохранении.
|
// затереть правки инициатора при первом сохранении.
|
||||||
const approval = useMemo(() => selectionApprovalService(), []);
|
const approval = useMemo(() => selectionApprovalService(), []);
|
||||||
const userEmail = context.pageContext.user.email;
|
const userEmail = effectiveUserEmail(context);
|
||||||
// pageUrl формы (requestType.id совпадает с ним по конвенции портала, см.
|
// pageUrl формы (requestType.id совпадает с ним по конвенции портала, см.
|
||||||
// data/requests.ts) → числовой RequestTypeId, нужный approval-route API.
|
// data/requests.ts) → числовой RequestTypeId, нужный approval-route API.
|
||||||
const requestTypeIdState = useRequestTypeId(context, requestType.id);
|
const requestTypeIdState = useRequestTypeId(context, requestType.id);
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
deleteAttachment,
|
deleteAttachment,
|
||||||
IAttachmentInfo
|
IAttachmentInfo
|
||||||
} from '../../shared/attachments/attachments.service';
|
} from '../../shared/attachments/attachments.service';
|
||||||
|
import { effectiveUserEmail } from '../../shared/impersonation';
|
||||||
import { IRequestFormProps } from '../../models/types';
|
import { IRequestFormProps } from '../../models/types';
|
||||||
import { IFormData, EMPTY_DATA, SetField } from './model';
|
import { IFormData, EMPTY_DATA, SetField } from './model';
|
||||||
import {
|
import {
|
||||||
@@ -132,7 +133,7 @@ const Ticket1CForm: React.FC<IRequestFormProps> = ({ requestType, context, onCan
|
|||||||
}, [editId]);
|
}, [editId]);
|
||||||
|
|
||||||
const approval = useMemo(() => ticket1cApprovalService(), []);
|
const approval = useMemo(() => ticket1cApprovalService(), []);
|
||||||
const userEmail = context.pageContext.user.email;
|
const userEmail = effectiveUserEmail(context);
|
||||||
// pageUrl формы (requestType.id совпадает с ним по конвенции портала, см.
|
// pageUrl формы (requestType.id совпадает с ним по конвенции портала, см.
|
||||||
// data/requests.ts) → числовой RequestTypeId, нужный approval-route API.
|
// data/requests.ts) → числовой RequestTypeId, нужный approval-route API.
|
||||||
const requestTypeIdState = useRequestTypeId(context, requestType.id);
|
const requestTypeIdState = useRequestTypeId(context, requestType.id);
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
deleteAttachment,
|
deleteAttachment,
|
||||||
IAttachmentInfo
|
IAttachmentInfo
|
||||||
} from '../../shared/attachments/attachments.service';
|
} from '../../shared/attachments/attachments.service';
|
||||||
|
import { effectiveUserEmail } from '../../shared/impersonation';
|
||||||
import { IRequestFormProps } from '../../models/types';
|
import { IRequestFormProps } from '../../models/types';
|
||||||
import { IFormData, EMPTY_DATA, SetField } from './model';
|
import { IFormData, EMPTY_DATA, SetField } from './model';
|
||||||
import { saveTmcTransferRequest, tmcTransferApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit';
|
import { saveTmcTransferRequest, tmcTransferApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit';
|
||||||
@@ -63,7 +64,10 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({ requestType, context, on
|
|||||||
const today = new Date().toISOString().slice(0, 10);
|
const today = new Date().toISOString().slice(0, 10);
|
||||||
setData((prev) => ({
|
setData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
RequesterEmail: user.email || '',
|
// email — через effectiveUserEmail (учитывает отладочную подмену личности);
|
||||||
|
// displayName подменить нечем, справочник ФИО тут не читается — при
|
||||||
|
// подмене имя останется настоящим, для отладки это неважно.
|
||||||
|
RequesterEmail: effectiveUserEmail(context),
|
||||||
AuthorName: user.displayName || '',
|
AuthorName: user.displayName || '',
|
||||||
RequestDate: prev.RequestDate || today
|
RequestDate: prev.RequestDate || today
|
||||||
}));
|
}));
|
||||||
@@ -95,7 +99,7 @@ const TmcTransferForm: React.FC<IRequestFormProps> = ({ requestType, context, on
|
|||||||
}, [editId]);
|
}, [editId]);
|
||||||
|
|
||||||
const approval = useMemo(() => tmcTransferApprovalService(), []);
|
const approval = useMemo(() => tmcTransferApprovalService(), []);
|
||||||
const userEmail = context.pageContext.user.email;
|
const userEmail = effectiveUserEmail(context);
|
||||||
// pageUrl формы (requestType.id совпадает с ним по конвенции портала, см.
|
// pageUrl формы (requestType.id совпадает с ним по конвенции портала, см.
|
||||||
// data/requests.ts) → числовой RequestTypeId, нужный approval-route API.
|
// data/requests.ts) → числовой RequestTypeId, нужный approval-route API.
|
||||||
const requestTypeIdState = useRequestTypeId(context, requestType.id);
|
const requestTypeIdState = useRequestTypeId(context, requestType.id);
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
deleteAttachment,
|
deleteAttachment,
|
||||||
IAttachmentInfo
|
IAttachmentInfo
|
||||||
} from '../../shared/attachments/attachments.service';
|
} from '../../shared/attachments/attachments.service';
|
||||||
|
import { effectiveUserEmail } from '../../shared/impersonation';
|
||||||
import { IRequestFormProps } from '../../models/types';
|
import { IRequestFormProps } from '../../models/types';
|
||||||
import { IFormData, EMPTY_DATA, SetField } from './model';
|
import { IFormData, EMPTY_DATA, SetField } from './model';
|
||||||
import { saveWorkplaceRequest, workplaceApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit';
|
import { saveWorkplaceRequest, workplaceApprovalService, DATA_LIST_TITLE, APPROVAL_LIST_TITLE } from './submit';
|
||||||
@@ -108,7 +109,7 @@ const WorkplaceSetupForm: React.FC<IRequestFormProps> = ({ requestType, context,
|
|||||||
}, [sourceId]);
|
}, [sourceId]);
|
||||||
|
|
||||||
const approval = useMemo(() => workplaceApprovalService(), []);
|
const approval = useMemo(() => workplaceApprovalService(), []);
|
||||||
const userEmail = context.pageContext.user.email;
|
const userEmail = effectiveUserEmail(context);
|
||||||
// pageUrl формы (requestType.id совпадает с ним по конвенции портала, см.
|
// pageUrl формы (requestType.id совпадает с ним по конвенции портала, см.
|
||||||
// data/requests.ts) → числовой RequestTypeId, нужный approval-route API.
|
// data/requests.ts) → числовой RequestTypeId, нужный approval-route API.
|
||||||
const requestTypeIdState = useRequestTypeId(context, requestType.id);
|
const requestTypeIdState = useRequestTypeId(context, requestType.id);
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ define([], function() {
|
|||||||
"PropertyPaneDescription": "Description",
|
"PropertyPaneDescription": "Description",
|
||||||
"BasicGroupName": "Group Name",
|
"BasicGroupName": "Group Name",
|
||||||
"DescriptionFieldLabel": "Description Field",
|
"DescriptionFieldLabel": "Description Field",
|
||||||
|
"DebugUserEmailFieldLabel": "Отладка: работать от имени сотрудника",
|
||||||
|
"DebugUserEmailFieldDescription": "Только для отладки маршрутов. Портал будет считать текущим пользователем указанный e-mail: по нему планируется маршрут, отбираются «свои» заявки и очередь согласования. Прав это не даёт — запись в списки всё равно идёт под вашей учёткой. На боевой странице оставьте пустым.",
|
||||||
"AppLocalEnvironmentSharePoint": "The app is running on your local environment as SharePoint web part",
|
"AppLocalEnvironmentSharePoint": "The app is running on your local environment as SharePoint web part",
|
||||||
"AppLocalEnvironmentTeams": "The app is running on your local environment as Microsoft Teams app",
|
"AppLocalEnvironmentTeams": "The app is running on your local environment as Microsoft Teams app",
|
||||||
"AppLocalEnvironmentOffice": "The app is running on your local environment in office.com",
|
"AppLocalEnvironmentOffice": "The app is running on your local environment in office.com",
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ declare interface IRequestPortalWebPartStrings {
|
|||||||
PropertyPaneDescription: string;
|
PropertyPaneDescription: string;
|
||||||
BasicGroupName: string;
|
BasicGroupName: string;
|
||||||
DescriptionFieldLabel: string;
|
DescriptionFieldLabel: string;
|
||||||
|
DebugUserEmailFieldLabel: string;
|
||||||
|
DebugUserEmailFieldDescription: string;
|
||||||
AppLocalEnvironmentSharePoint: string;
|
AppLocalEnvironmentSharePoint: string;
|
||||||
AppLocalEnvironmentTeams: string;
|
AppLocalEnvironmentTeams: string;
|
||||||
AppLocalEnvironmentOffice: string;
|
AppLocalEnvironmentOffice: string;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { IList } from '@pnp/sp/lists';
|
|||||||
import '@pnp/sp/items';
|
import '@pnp/sp/items';
|
||||||
import '@pnp/sp/site-users/web';
|
import '@pnp/sp/site-users/web';
|
||||||
import '@pnp/sp/profiles';
|
import '@pnp/sp/profiles';
|
||||||
|
import { getImpersonatedEmail } from '../impersonation';
|
||||||
import { IApprovalRow } from './models/IApprovalRow';
|
import { IApprovalRow } from './models/IApprovalRow';
|
||||||
import { IMatrixRow } from './models/IMatrixRow';
|
import { IMatrixRow } from './models/IMatrixRow';
|
||||||
import { ISiteUser } from './models/ISiteUser';
|
import { ISiteUser } from './models/ISiteUser';
|
||||||
@@ -349,7 +350,13 @@ export class ApprovalListService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Id текущего пользователя на сайте (для сравнения с полем «Согласующий»).
|
// Id текущего пользователя на сайте (для сравнения с полем «Согласующий»).
|
||||||
|
// При отладочной подмене личности (см. shared/impersonation.ts) отдаём Id
|
||||||
|
// подменённого сотрудника: иначе «моя очередь согласования» и делегирование
|
||||||
|
// считались бы по реальной учётке, и подмена не позволяла бы проверить шаг
|
||||||
|
// чужого согласующего — ради чего она и заводилась.
|
||||||
public async getCurrentUserId(): Promise<number> {
|
public async getCurrentUserId(): Promise<number> {
|
||||||
|
const impersonated = getImpersonatedEmail();
|
||||||
|
if (impersonated) return this.resolveUserId(impersonated);
|
||||||
const user = await getSP().web.currentUser.select('Id')();
|
const user = await getSP().web.currentUser.select('Id')();
|
||||||
return user.Id;
|
return user.Id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ОТЛАДОЧНАЯ подмена «текущего пользователя»: портал ведёт себя так, будто им
|
||||||
|
* пользуется указанный сотрудник. Нужна, чтобы проверять маршруты и права
|
||||||
|
* согласующего (например, «карту СКУД правит начальник КДП» в обходном листе),
|
||||||
|
* не выпрашивая учётку у каждого участника маршрута.
|
||||||
|
*
|
||||||
|
* Значение задаётся в свойствах веб-части (property pane, см.
|
||||||
|
* RequestPortalWebPart.getPropertyPaneConfiguration) и живёт на уровне модуля —
|
||||||
|
* иначе его пришлось бы протаскивать пропсами через всё дерево до каждой формы.
|
||||||
|
*
|
||||||
|
* ЧТО ЭТО МЕНЯЕТ: только то, ЧЕЙ email портал отправляет как инициатора при
|
||||||
|
* планировании маршрута и по какому пользователю фильтрует «свои» заявки и
|
||||||
|
* очередь согласования.
|
||||||
|
*
|
||||||
|
* ЧЕГО ЭТО НЕ МЕНЯЕТ: прав доступа. Запись в SharePoint по-прежнему идёт под
|
||||||
|
* реальной учёткой (Author/Editor в списках — настоящий пользователь), токен к
|
||||||
|
* approval-route API тоже настоящий. То есть подмена не даёт увидеть то, к чему
|
||||||
|
* нет доступа, но ПОЗВОЛЯЕТ принять решение за чужой шаг согласования — поэтому
|
||||||
|
* на бою поле должно быть пустым (см. предупреждающую полосу в RequestPortal).
|
||||||
|
*/
|
||||||
|
|
||||||
|
let impersonatedEmail = '';
|
||||||
|
|
||||||
|
/** Вызывается веб-частью на каждый render — свойство могли поменять в панели. */
|
||||||
|
export function setImpersonatedEmail(email: string | undefined): void {
|
||||||
|
impersonatedEmail = (email || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Заданная подмена ('' — подмены нет). Для предупреждающей полосы в интерфейсе. */
|
||||||
|
export function getImpersonatedEmail(): string {
|
||||||
|
return impersonatedEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Email, под которым портал работает: подменённый, если задан, иначе — из токена. */
|
||||||
|
export function effectiveUserEmail(context: WebPartContext): string {
|
||||||
|
return impersonatedEmail || context.pageContext.user.email || '';
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user