Roadmap/#1379 admin ui doc level access (#1624)

* feat: adds document level access endpoints so admin ui can now accurately reflect document level access control
* chore(docs): new doc access callout, updates useDocumentInfo props from change
This commit is contained in:
Jarrod Flesch
2022-12-14 16:14:29 -05:00
committed by GitHub
parent d9c45f62b1
commit eda6f70acb
23 changed files with 702 additions and 154 deletions

View File

@@ -5,12 +5,13 @@ import qs from 'qs';
import { useTranslation } from 'react-i18next';
import { useConfig } from '../Config';
import { PaginatedDocs } from '../../../../mongoose/types';
import { ContextType, Props, Version } from './types';
import { ContextType, DocumentPermissions, EntityType, Props, Version } from './types';
import { TypeWithID } from '../../../../globals/config/types';
import { TypeWithTimestamps } from '../../../../collections/config/types';
import { Where } from '../../../../types';
import { DocumentPreferences } from '../../../../preferences/types';
import { usePreferences } from '../Preferences';
import { useAuth } from '../Auth';
const Context = createContext({} as ContextType);
@@ -23,24 +24,29 @@ export const DocumentInfoProvider: React.FC<Props> = ({
const { serverURL, routes: { api } } = useConfig();
const { getPreference } = usePreferences();
const { i18n } = useTranslation();
const { permissions } = useAuth();
const [publishedDoc, setPublishedDoc] = useState<TypeWithID & TypeWithTimestamps>(null);
const [versions, setVersions] = useState<PaginatedDocs<Version>>(null);
const [unpublishedVersions, setUnpublishedVersions] = useState<PaginatedDocs<Version>>(null);
const [docPermissions, setDocPermissions] = useState<DocumentPermissions>(null);
const baseURL = `${serverURL}${api}`;
let slug;
let type;
let preferencesKey;
let slug: string;
let type: EntityType;
let pluralType: 'globals' | 'collections';
let preferencesKey: string;
if (global) {
slug = global.slug;
type = 'global';
pluralType = 'globals';
preferencesKey = `global-${slug}`;
}
if (collection) {
slug = collection.slug;
type = 'collection';
pluralType = 'collections';
if (id) {
preferencesKey = `collection-${slug}-${id}`;
@@ -169,6 +175,25 @@ export const DocumentInfoProvider: React.FC<Props> = ({
}
}, [i18n, global, collection, id, baseURL]);
const getDocPermissions = React.useCallback(async () => {
let docAccessURL: string;
if (pluralType === 'globals') {
docAccessURL = `/globals/${slug}/access`;
} else if (pluralType === 'collections' && id) {
docAccessURL = `/${slug}/access/${id}`;
}
if (docAccessURL) {
const res = await fetch(`${serverURL}${api}${docAccessURL}`);
const json = await res.json();
setDocPermissions(json);
} else {
// fallback to permissions from the collection
// (i.e. create has no id)
setDocPermissions(permissions[pluralType][slug]);
}
}, [serverURL, api, pluralType, slug, id, permissions]);
useEffect(() => {
getVersions();
}, [getVersions]);
@@ -181,6 +206,10 @@ export const DocumentInfoProvider: React.FC<Props> = ({
getDocPreferences();
}, [getPreference, preferencesKey]);
useEffect(() => {
getDocPermissions();
}, [getDocPermissions]);
const value = {
slug,
type,
@@ -192,6 +221,8 @@ export const DocumentInfoProvider: React.FC<Props> = ({
getVersions,
publishedDoc,
id,
getDocPermissions,
docPermissions,
};
return (
@@ -202,5 +233,3 @@ export const DocumentInfoProvider: React.FC<Props> = ({
};
export const useDocumentInfo = (): ContextType => useContext(Context);
export default Context;

View File

@@ -1,4 +1,5 @@
import React from 'react';
import { CollectionPermission, GlobalPermission } from '../../../../auth';
import { SanitizedCollectionConfig, TypeWithID, TypeWithTimestamps } from '../../../../collections/config/types';
import { SanitizedGlobalConfig } from '../../../../globals/config/types';
import { PaginatedDocs } from '../../../../mongoose/types';
@@ -6,10 +7,14 @@ import { TypeWithVersion } from '../../../../versions/types';
export type Version = TypeWithVersion<any>
export type DocumentPermissions = null | GlobalPermission | CollectionPermission
export type EntityType = 'global' | 'collection'
export type ContextType = {
collection?: SanitizedCollectionConfig
global?: SanitizedGlobalConfig
type: 'global' | 'collection'
type: EntityType
/** Slug of the collection or global */
slug?: string
id?: string | number
@@ -18,6 +23,8 @@ export type ContextType = {
unpublishedVersions?: PaginatedDocs<Version>
publishedDoc?: TypeWithID & TypeWithTimestamps & { _status?: string }
getVersions: () => Promise<void>
docPermissions: DocumentPermissions
getDocPermissions: () => Promise<void>
}
export type Props = {

View File

@@ -20,9 +20,9 @@ const GlobalView: React.FC<IndexProps> = (props) => {
const { state: locationState } = useLocation<{data?: Record<string, unknown>}>();
const locale = useLocale();
const { setStepNav } = useStepNav();
const { permissions, user } = useAuth();
const { user } = useAuth();
const [initialState, setInitialState] = useState<Fields>();
const { getVersions, preferencesKey } = useDocumentInfo();
const { getVersions, preferencesKey, docPermissions, getDocPermissions } = useDocumentInfo();
const { getPreference } = usePreferences();
const { t } = useTranslation();
@@ -50,9 +50,10 @@ const GlobalView: React.FC<IndexProps> = (props) => {
const onSave = useCallback(async (json) => {
getVersions();
getDocPermissions();
const state = await buildStateFromSchema({ fieldSchema: fields, data: json.result, operation: 'update', user, locale, t });
setInitialState(state);
}, [getVersions, fields, user, locale, t]);
}, [getVersions, fields, user, locale, t, getDocPermissions]);
const [{ data }] = usePayloadAPI(
`${serverURL}${api}/globals/${slug}`,
@@ -79,16 +80,14 @@ const GlobalView: React.FC<IndexProps> = (props) => {
awaitInitialState();
}, [dataToRender, fields, user, locale, getPreference, preferencesKey, t]);
const globalPermissions = permissions?.globals?.[slug];
return (
<RenderCustomComponent
DefaultComponent={DefaultGlobal}
CustomComponent={CustomEdit}
componentProps={{
isLoading: !initialState,
isLoading: !initialState || !docPermissions,
data: dataToRender,
permissions: globalPermissions,
permissions: docPermissions,
initialState,
global,
onSave,

View File

@@ -15,6 +15,7 @@ import { useDocumentInfo } from '../../../utilities/DocumentInfo';
import { Fields } from '../../../forms/Form/types';
import { usePreferences } from '../../../utilities/Preferences';
import { EditDepthContext } from '../../../utilities/EditDepth';
import { CollectionPermission } from '../../../../../auth';
const EditView: React.FC<IndexProps> = (props) => {
const { collection: incomingCollection, isEditing } = props;
@@ -40,20 +41,21 @@ const EditView: React.FC<IndexProps> = (props) => {
const { state: locationState } = useLocation();
const history = useHistory();
const [initialState, setInitialState] = useState<Fields>();
const { permissions, user } = useAuth();
const { getVersions, preferencesKey } = useDocumentInfo();
const { user } = useAuth();
const { getVersions, preferencesKey, getDocPermissions, docPermissions } = useDocumentInfo();
const { getPreference } = usePreferences();
const { t } = useTranslation('general');
const onSave = useCallback(async (json: any) => {
getVersions();
getDocPermissions();
if (!isEditing) {
setRedirect(`${admin}/collections/${collection.slug}/${json?.doc?.id}`);
} else {
const state = await buildStateFromSchema({ fieldSchema: collection.fields, data: json.doc, user, id, operation: 'update', locale, t });
setInitialState(state);
}
}, [admin, collection, isEditing, getVersions, user, id, t, locale]);
}, [admin, collection, isEditing, getVersions, user, id, t, locale, getDocPermissions]);
const [{ data, isLoading: isLoadingDocument, isError }] = usePayloadAPI(
(isEditing ? `${serverURL}${api}/${slug}/${id}` : null),
@@ -87,10 +89,9 @@ const EditView: React.FC<IndexProps> = (props) => {
);
}
const collectionPermissions = permissions?.collections?.[slug];
const apiURL = `${serverURL}${api}/${slug}/${id}${collection.versions.drafts ? '?draft=true' : ''}`;
const action = `${serverURL}${api}/${slug}${isEditing ? `/${id}` : ''}?locale=${locale}&depth=0&fallback-locale=null`;
const hasSavePermission = (isEditing && collectionPermissions?.update?.permission) || (!isEditing && collectionPermissions?.create?.permission);
const hasSavePermission = (isEditing && docPermissions?.update?.permission) || (!isEditing && (docPermissions as CollectionPermission)?.create?.permission);
return (
<EditDepthContext.Provider value={1}>
@@ -99,10 +100,10 @@ const EditView: React.FC<IndexProps> = (props) => {
CustomComponent={CustomEdit}
componentProps={{
id,
isLoading: !initialState,
isLoading: !initialState || !docPermissions,
data: dataToRender,
collection,
permissions: collectionPermissions,
permissions: docPermissions,
isEditing,
onSave,
initialState,