125 lines
5.5 KiB
TypeScript
125 lines
5.5 KiB
TypeScript
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
|
import { SPHttpClient } from '@microsoft/sp-http';
|
|
|
|
/**
|
|
* ОТЛАДОЧНАЯ подмена «текущего пользователя»: портал ведёт себя так, будто им
|
|
* пользуется указанный сотрудник. Нужна, чтобы проверять маршруты и права
|
|
* согласующего (например, «карту СКУД правит начальник КДП» в обходном листе),
|
|
* не выпрашивая учётку у каждого участника маршрута.
|
|
*
|
|
* Значение задаётся в свойствах веб-части (property pane, см.
|
|
* RequestPortalWebPart.getPropertyPaneConfiguration) и живёт на уровне модуля —
|
|
* иначе его пришлось бы протаскивать пропсами через всё дерево до каждой формы.
|
|
*
|
|
* ЧТО ЭТО МЕНЯЕТ: только то, ЧЕЙ email портал отправляет как инициатора при
|
|
* планировании маршрута и по какому пользователю фильтрует «свои» заявки и
|
|
* очередь согласования.
|
|
*
|
|
* ЧЕГО ЭТО НЕ МЕНЯЕТ: прав доступа. Запись в SharePoint по-прежнему идёт под
|
|
* реальной учёткой (Author/Editor в списках — настоящий пользователь), токен к
|
|
* approval-route API тоже настоящий. То есть подмена не даёт увидеть то, к чему
|
|
* нет доступа, но ПОЗВОЛЯЕТ принять решение за чужой шаг согласования — поэтому
|
|
* на бою поле должно быть пустым (см. предупреждающую полосу в RequestPortal).
|
|
*/
|
|
|
|
let impersonatedEmail = '';
|
|
let impersonatedDisplayName = '';
|
|
|
|
/** Вызывается веб-частью на каждый render — свойство могли поменять в панели. */
|
|
export function setImpersonatedIdentity(email: string | undefined, displayName?: string): void {
|
|
impersonatedEmail = (email || '').trim();
|
|
impersonatedDisplayName = (displayName || '').trim();
|
|
}
|
|
|
|
/** Заданная подмена ('' — подмены нет). Для предупреждающей полосы в интерфейсе. */
|
|
export function getImpersonatedEmail(): string {
|
|
return impersonatedEmail;
|
|
}
|
|
|
|
/** ФИО подменённого сотрудника, если его удалось найти поиском по имени ('' — для e-mail, введённого напрямую). */
|
|
export function getImpersonatedDisplayName(): string {
|
|
return impersonatedDisplayName;
|
|
}
|
|
|
|
/** Email, под которым портал работает: подменённый, если задан, иначе — из токена. */
|
|
export function effectiveUserEmail(context: WebPartContext): string {
|
|
return impersonatedEmail || context.pageContext.user.email || '';
|
|
}
|
|
|
|
export interface IResolvedDebugUser {
|
|
email: string;
|
|
displayName: string;
|
|
}
|
|
|
|
interface IPeopleSearchHit {
|
|
displayName: string;
|
|
email: string;
|
|
}
|
|
|
|
/** Сырой ответ ClientPeoplePickerSearchUser — value внутри двойного JSON. */
|
|
interface IPeoplePickerEntity {
|
|
DisplayText?: string;
|
|
Description?: string;
|
|
EntityData?: { Email?: string };
|
|
}
|
|
|
|
async function searchPeopleByText(context: WebPartContext, queryText: string): Promise<IPeopleSearchHit[]> {
|
|
const url = `${context.pageContext.web.absoluteUrl}/_api/SP.UI.ApplicationPages.ClientPeoplePickerWebServiceInterface.clientPeoplePickerSearchUser`;
|
|
const body = JSON.stringify({
|
|
queryParams: {
|
|
AllowEmailAddresses: true,
|
|
AllowMultipleEntities: false,
|
|
MaximumEntitySuggestions: 5,
|
|
PrincipalSource: 15, // All
|
|
PrincipalType: 1, // User
|
|
QueryString: queryText
|
|
}
|
|
});
|
|
|
|
const response = await context.spHttpClient.post(url, SPHttpClient.configurations.v1, {
|
|
headers: {
|
|
Accept: 'application/json;odata=nometadata',
|
|
'Content-Type': 'application/json;odata=nometadata'
|
|
},
|
|
body
|
|
});
|
|
|
|
if (!response.ok) return [];
|
|
|
|
const json: { value?: string } = await response.json();
|
|
if (!json.value) return [];
|
|
|
|
let entries: IPeoplePickerEntity[];
|
|
try {
|
|
entries = JSON.parse(json.value);
|
|
} catch {
|
|
return [];
|
|
}
|
|
|
|
return entries
|
|
.map((e) => ({ displayName: e.DisplayText || '', email: e.EntityData?.Email || e.Description || '' }))
|
|
.filter((e) => !!e.email);
|
|
}
|
|
|
|
/**
|
|
* Ищет сотрудника по имени для отладочной подмены (см. property pane —
|
|
* PropertyPaneDebugUser/DebugUserField). Порядок «Фамилия Имя» или «Имя
|
|
* Фамилия» не важен: если прямой запрос ничего не находит, пробуем с
|
|
* переставленными первыми двумя словами.
|
|
*/
|
|
export async function resolveDebugUser(context: WebPartContext, nameQuery: string): Promise<IResolvedDebugUser | undefined> {
|
|
const text = nameQuery.trim();
|
|
if (!text) return undefined;
|
|
|
|
let hits = await searchPeopleByText(context, text);
|
|
if (hits.length === 0) {
|
|
const tokens = text.split(/\s+/).filter(Boolean);
|
|
if (tokens.length >= 2) {
|
|
const swapped = [tokens[1], tokens[0], ...tokens.slice(2)].join(' ');
|
|
hits = await searchPeopleByText(context, swapped);
|
|
}
|
|
}
|
|
|
|
return hits[0];
|
|
}
|