fix: check report access with the user id of the lists site
The user id differs per site, so the id from the page context showed "Нет доступа" in reports when the page was not on /sites/stroy. The id and the photo folder path now come from settings.siteUrl. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -2,5 +2,5 @@
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/spfx-serve.schema.json",
|
||||
"port": 4321,
|
||||
"https": true,
|
||||
"initialPage": "https://tnsukz.sharepoint.com/_layouts/workbench.aspx"
|
||||
"initialPage": "https://tnsukz.sharepoint.com/sites/stroy/_layouts/workbench.aspx"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { IAppServices } from '../../services/AppServices';
|
||||
import { siteServerRelativeUrl } from '../../services/siteUser';
|
||||
import { createSharePointReportRepository, ISharePointReportRepository } from './sharePointReportRepository';
|
||||
import { createDataverseReportRepository, IDataverseReportRepository } from './dataverseReportRepository';
|
||||
|
||||
@@ -9,13 +10,15 @@ export interface IReportServices {
|
||||
siteUrl: string;
|
||||
}
|
||||
|
||||
// Всё — от сайта со списками (settings.siteUrl), а не от сайта страницы: в workbench корневого
|
||||
// сайта id пользователя и путь к библиотеке DocLib там другие.
|
||||
export function createReportServices(services: IAppServices): IReportServices {
|
||||
return {
|
||||
sp: createSharePointReportRepository({
|
||||
client: services.sharePoint,
|
||||
siteUrl: services.settings.siteUrl,
|
||||
userId: services.page.userId,
|
||||
webServerRelativeUrl: services.page.webServerRelativeUrl
|
||||
getCurrentUserId: services.getCurrentUserId,
|
||||
webServerRelativeUrl: siteServerRelativeUrl(services.settings.siteUrl)
|
||||
}),
|
||||
dv: createDataverseReportRepository(services.dataverse, services.settings.dataverseUrl),
|
||||
siteUrl: services.settings.siteUrl
|
||||
|
||||
@@ -17,7 +17,7 @@ const repositoryWith = (routes: IFakeRoute[]): {
|
||||
const repository = createSharePointReportRepository({
|
||||
client,
|
||||
siteUrl: SITE,
|
||||
userId: 42,
|
||||
getCurrentUserId: async () => 42,
|
||||
webServerRelativeUrl: '/sites/stroy/'
|
||||
});
|
||||
return { client, repository };
|
||||
|
||||
@@ -111,9 +111,13 @@ export function fileAddUrl(siteUrl: string, serverRelativeFolderUrl: string, fil
|
||||
export interface ISharePointReportDeps {
|
||||
client: IHttpClient;
|
||||
siteUrl: string;
|
||||
/** Id текущего пользователя (legacyPageContext.userId) — по нему ищется строка «Доступы». */
|
||||
userId: number | undefined;
|
||||
/** Серверный относительный URL сайта — для пути к папке фото. */
|
||||
/**
|
||||
* Id текущего пользователя на сайте со списками — по нему ищется строка «Доступы».
|
||||
* Старый код брал legacyPageContext.userId (id на сайте страницы); в workbench корневого сайта
|
||||
* это другой id, и доступ не находился. Теперь — `_api/web/currentuser` сайта со списками.
|
||||
*/
|
||||
getCurrentUserId: () => Promise<number>;
|
||||
/** Серверный относительный URL сайта со списками (`/sites/stroy`) — для пути к папке фото. */
|
||||
webServerRelativeUrl: string;
|
||||
}
|
||||
|
||||
@@ -224,8 +228,11 @@ export function createSharePointReportRepository(deps: ISharePointReportDeps): I
|
||||
createListItem,
|
||||
|
||||
checkAccess: async (projectId) => {
|
||||
const rows = await getItems<IAccessRaw>(LISTS.access, { filter: `projectId eq ${projectId}` });
|
||||
return pickAccess(rows, deps.userId);
|
||||
const [rows, userId] = await Promise.all([
|
||||
getItems<IAccessRaw>(LISTS.access, { filter: `projectId eq ${projectId}` }),
|
||||
deps.getCurrentUserId()
|
||||
]);
|
||||
return pickAccess(rows, userId);
|
||||
},
|
||||
|
||||
uploadPhotos: async (projectId, reportNumber, photos) => {
|
||||
|
||||
@@ -11,7 +11,8 @@ export interface IReportListDeps {
|
||||
dataverse: IHttpClient;
|
||||
siteUrl: string;
|
||||
dataverseUrl: string;
|
||||
userId: number | undefined;
|
||||
/** Id пользователя на сайте со списками (не на сайте страницы) — см. IAppServices.getCurrentUserId. */
|
||||
getCurrentUserId: () => Promise<number>;
|
||||
}
|
||||
|
||||
/** Справочники для подписей карточек: компания по Id, путь объекта. */
|
||||
@@ -49,7 +50,8 @@ export async function loadReportList(
|
||||
projectId: number,
|
||||
range: IDateRange
|
||||
): Promise<ReportListInitResult> {
|
||||
const accessResult = await fetchReportAccess(deps.sharePoint, deps.siteUrl, deps.userId, projectId);
|
||||
const userId = await deps.getCurrentUserId();
|
||||
const accessResult = await fetchReportAccess(deps.sharePoint, deps.siteUrl, userId, projectId);
|
||||
if (!accessResult) return { kind: 'denied' };
|
||||
|
||||
const base = { kind: 'ready' as const, projectName: accessResult.projectName, access: accessResult.access };
|
||||
|
||||
@@ -149,7 +149,7 @@ describe('loadReportList', () => {
|
||||
}
|
||||
}
|
||||
]);
|
||||
const deps = { sharePoint, dataverse, siteUrl: SITE, dataverseUrl: DV, userId: 42 };
|
||||
const deps = { sharePoint, dataverse, siteUrl: SITE, dataverseUrl: DV, getCurrentUserId: async () => 42 };
|
||||
|
||||
const result = await loadReportList(deps, 5, WEEK);
|
||||
|
||||
@@ -174,7 +174,7 @@ describe('loadReportList', () => {
|
||||
it('нет доступа — denied, справочники не грузятся', async () => {
|
||||
const sharePoint = createFakeHttpClient([{ match: 'Доступы', response: { body: { value: [] } } }]);
|
||||
const dataverse = createFakeHttpClient([]);
|
||||
const result = await loadReportList({ sharePoint, dataverse, siteUrl: SITE, dataverseUrl: DV, userId: 42 }, 5, WEEK);
|
||||
const result = await loadReportList({ sharePoint, dataverse, siteUrl: SITE, dataverseUrl: DV, getCurrentUserId: async () => 42 }, 5, WEEK);
|
||||
expect(result).toEqual({ kind: 'denied' });
|
||||
expect(sharePoint.requests).toHaveLength(1);
|
||||
});
|
||||
@@ -182,7 +182,7 @@ describe('loadReportList', () => {
|
||||
it('ошибка отчётов — экран с «Ошибка инициализации: …» и без карточек', async () => {
|
||||
const sharePoint = createFakeHttpClient([access, companies, objects]);
|
||||
const dataverse = createFakeHttpClient([{ match: 'cree9_reports', response: { status: 500, body: 'fail' } }]);
|
||||
const result = await loadReportList({ sharePoint, dataverse, siteUrl: SITE, dataverseUrl: DV, userId: 42 }, 5, WEEK);
|
||||
const result = await loadReportList({ sharePoint, dataverse, siteUrl: SITE, dataverseUrl: DV, getCurrentUserId: async () => 42 }, 5, WEEK);
|
||||
expect(result).toMatchObject({
|
||||
kind: 'ready',
|
||||
reports: undefined,
|
||||
|
||||
@@ -26,7 +26,7 @@ export interface IReportListState {
|
||||
|
||||
/** Данные страницы отчётов: первая загрузка, «Применить» и удаление. */
|
||||
export function useReportList(projectId: number, initialRange: IDateRange): IReportListState {
|
||||
const { sharePoint, dataverse, settings, page } = useAppServices();
|
||||
const { sharePoint, dataverse, settings, getCurrentUserId } = useAppServices();
|
||||
const [phase, setPhase] = React.useState<ReportListPhase>('loading');
|
||||
const [projectName, setProjectName] = React.useState<string>('');
|
||||
const [access, setAccess] = React.useState<IReportAccess>(NO_ACCESS);
|
||||
@@ -44,9 +44,9 @@ export function useReportList(projectId: number, initialRange: IDateRange): IRep
|
||||
dataverse,
|
||||
siteUrl: settings.siteUrl,
|
||||
dataverseUrl: settings.dataverseUrl,
|
||||
userId: page.userId
|
||||
getCurrentUserId
|
||||
}),
|
||||
[sharePoint, dataverse, settings.siteUrl, settings.dataverseUrl, page.userId]
|
||||
[sharePoint, dataverse, settings.siteUrl, settings.dataverseUrl, getCurrentUserId]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { IHttpClient } from './http/IHttpClient';
|
||||
import { createAadClient, createSharePointClient } from './http/spfxClients';
|
||||
import { DATAVERSE_URL, SITE_URL, TNSU_API_BASE_URL, TNSU_API_RESOURCE_ID } from '../config/constants';
|
||||
import { createCurrentUserIdProvider } from './siteUser';
|
||||
|
||||
/** Настройки подключения из property pane (пустое значение — берётся значение по умолчанию). */
|
||||
export interface IConnectionSettings {
|
||||
@@ -34,6 +35,12 @@ export interface IAppServices {
|
||||
tnsuApi: IHttpClient;
|
||||
settings: IResolvedSettings;
|
||||
page: IPageInfo;
|
||||
/**
|
||||
* Id текущего пользователя на сайте со списками (settings.siteUrl). Id пользователя у каждого сайта
|
||||
* свой: legacyPageContext.userId — это id на сайте страницы, и в workbench корневого сайта он не
|
||||
* совпадает с id в «Доступах» сайта stroy. Запрашивается один раз.
|
||||
*/
|
||||
getCurrentUserId: () => Promise<number>;
|
||||
}
|
||||
|
||||
function withoutTrailingSlash(url: string): string {
|
||||
@@ -60,11 +67,13 @@ function readPageInfo(context: WebPartContext): IPageInfo {
|
||||
|
||||
export function createAppServices(context: WebPartContext, settings: IConnectionSettings): IAppServices {
|
||||
const resolved = resolveSettings(settings);
|
||||
const sharePoint = createSharePointClient(context);
|
||||
return {
|
||||
sharePoint: createSharePointClient(context),
|
||||
sharePoint,
|
||||
dataverse: createAadClient(context, resolved.dataverseUrl),
|
||||
tnsuApi: createAadClient(context, TNSU_API_RESOURCE_ID),
|
||||
settings: resolved,
|
||||
page: readPageInfo(context)
|
||||
page: readPageInfo(context),
|
||||
getCurrentUserId: createCurrentUserIdProvider(sharePoint, resolved.siteUrl)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { createCurrentUserIdProvider, siteServerRelativeUrl } from './siteUser';
|
||||
import { createFakeHttpClient } from '../shared/testing/fakeHttpClient';
|
||||
|
||||
const SITE = 'https://tnsukz.sharepoint.com/sites/stroy';
|
||||
|
||||
describe('siteServerRelativeUrl', () => {
|
||||
it('путь сайта без домена', () => {
|
||||
expect(siteServerRelativeUrl(SITE)).toEqual('/sites/stroy');
|
||||
expect(siteServerRelativeUrl('https://tnsukz.sharepoint.com')).toEqual('/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createCurrentUserIdProvider', () => {
|
||||
it('спрашивает currentuser у сайта со списками (не у сайта страницы) и запоминает', async () => {
|
||||
const sharePoint = createFakeHttpClient([{ match: 'currentuser', response: { body: { Id: 19 } } }]);
|
||||
const getCurrentUserId = createCurrentUserIdProvider(sharePoint, SITE);
|
||||
|
||||
expect(await getCurrentUserId()).toEqual(19);
|
||||
expect(await getCurrentUserId()).toEqual(19);
|
||||
expect(sharePoint.requests).toHaveLength(1);
|
||||
expect(sharePoint.requests[0].url).toEqual(`${SITE}/_api/web/currentuser?$select=Id`);
|
||||
});
|
||||
|
||||
it('неудачный запрос не запоминается — следующий вызов спрашивает снова', async () => {
|
||||
const sharePoint = createFakeHttpClient([
|
||||
{ match: 'currentuser', respond: (call) => (call === 1 ? { status: 503 } : { body: { Id: 19 } }) }
|
||||
]);
|
||||
const getCurrentUserId = createCurrentUserIdProvider(sharePoint, SITE);
|
||||
|
||||
await expect(getCurrentUserId()).rejects.toThrow('503');
|
||||
expect(await getCurrentUserId()).toEqual(19);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { IHttpClient } from './http/IHttpClient';
|
||||
import { fetchCurrentUserId } from '../shared/sharepoint/spRest';
|
||||
|
||||
// Пользователь и пути — всегда относительно сайта со списками (settings.siteUrl), а не сайта страницы:
|
||||
// id пользователя у каждого сайта свой, и в workbench корневого сайта он не совпадает с id в «Доступах».
|
||||
|
||||
/** Серверный относительный путь сайта (`/sites/stroy`) — для путей к папкам библиотек. */
|
||||
export function siteServerRelativeUrl(siteUrl: string): string {
|
||||
return siteUrl.replace(/^https?:\/\/[^/]+/, '') || '/';
|
||||
}
|
||||
|
||||
/** Запоминает id пользователя; неудачный запрос не запоминается — следующий вызов спросит снова. */
|
||||
export function createCurrentUserIdProvider(sharePoint: IHttpClient, siteUrl: string): () => Promise<number> {
|
||||
let userIdPromise: Promise<number> | undefined;
|
||||
return () => {
|
||||
if (userIdPromise) return userIdPromise;
|
||||
const request = fetchCurrentUserId(sharePoint, siteUrl).catch((error: unknown) => {
|
||||
userIdPromise = undefined;
|
||||
throw error;
|
||||
});
|
||||
userIdPromise = request;
|
||||
return request;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user