mirror of
https://github.com/strapi/strapi.git
synced 2025-11-02 02:44:55 +00:00
Merge pull request #21310 from strapi/fix/i18n-rbac-locale-switch
Use correct user permissions when switching between locales
This commit is contained in:
commit
f2ff35145b
@ -6,6 +6,7 @@ import { Login } from '../../../shared/contracts/authentication';
|
||||
import { createContext } from '../components/Context';
|
||||
import { useTypedDispatch, useTypedSelector } from '../core/store/hooks';
|
||||
import { useStrapiApp } from '../features/StrapiApp';
|
||||
import { useQueryParams } from '../hooks/useQueryParams';
|
||||
import { login as loginAction, logout as logoutAction, setLocale } from '../reducer';
|
||||
import { adminApi } from '../services/api';
|
||||
import {
|
||||
@ -45,7 +46,8 @@ interface AuthContextValue {
|
||||
*/
|
||||
checkUserHasPermissions: (
|
||||
permissions?: Permission[],
|
||||
passedPermissions?: Permission[]
|
||||
passedPermissions?: Permission[],
|
||||
rawQueryContext?: string
|
||||
) => Promise<Permission[]>;
|
||||
isLoading: boolean;
|
||||
permissions: Permission[];
|
||||
@ -80,6 +82,8 @@ const AuthProvider = ({
|
||||
const dispatch = useTypedDispatch();
|
||||
const runRbacMiddleware = useStrapiApp('AuthProvider', (state) => state.rbac.run);
|
||||
const location = useLocation();
|
||||
const [{ rawQuery }] = useQueryParams();
|
||||
|
||||
const token = useTypedSelector((state) => state.admin_app.token ?? null);
|
||||
|
||||
const { data: user, isLoading: isLoadingUser } = useGetMeQuery(undefined, {
|
||||
@ -194,7 +198,20 @@ const AuthProvider = ({
|
||||
|
||||
const [checkPermissions] = useLazyCheckPermissionsQuery();
|
||||
const checkUserHasPermissions: AuthContextValue['checkUserHasPermissions'] = React.useCallback(
|
||||
async (permissions, passedPermissions) => {
|
||||
async (
|
||||
permissions,
|
||||
passedPermissions,
|
||||
// TODO:
|
||||
// Here we have parameterised checkUserHasPermissions in order to pass
|
||||
// query context from elsewhere in the application.
|
||||
// See packages/core/content-manager/admin/src/features/DocumentRBAC.tsx
|
||||
|
||||
// This is in order to calculate permissions on accurate query params.
|
||||
// We should be able to rely on the query params in this provider
|
||||
// If we need to pass additional context to the RBAC middleware
|
||||
// we should define a better context type.
|
||||
rawQueryContext
|
||||
) => {
|
||||
/**
|
||||
* If there's no permissions to check, then we allow it to
|
||||
* pass to preserve existing behaviours.
|
||||
@ -223,7 +240,7 @@ const AuthProvider = ({
|
||||
user,
|
||||
permissions: userPermissions,
|
||||
pathname: location.pathname,
|
||||
search: location.search.split('?')[1] ?? '',
|
||||
search: (rawQueryContext || rawQuery).split('?')[1] ?? '',
|
||||
},
|
||||
matchingPermissions
|
||||
);
|
||||
@ -249,7 +266,7 @@ const AuthProvider = ({
|
||||
return middlewaredPermissions.filter((_, index) => data?.data[index] === true);
|
||||
}
|
||||
},
|
||||
[checkPermissions, location.pathname, location.search, runRbacMiddleware, user, userPermissions]
|
||||
[checkPermissions, location.pathname, rawQuery, runRbacMiddleware, user, userPermissions]
|
||||
);
|
||||
|
||||
const isLoading = isLoadingUser || isLoadingPermissions;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import * as React from 'react';
|
||||
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
@ -43,7 +43,8 @@ type AllowedActions = Record<string, boolean>;
|
||||
*/
|
||||
const useRBAC = (
|
||||
permissionsToCheck: Record<string, Permission[]> | Permission[] = [],
|
||||
passedPermissions?: Permission[]
|
||||
passedPermissions?: Permission[],
|
||||
rawQueryContext?: string
|
||||
): {
|
||||
allowedActions: AllowedActions;
|
||||
isLoading: boolean;
|
||||
@ -51,13 +52,13 @@ const useRBAC = (
|
||||
permissions: Permission[];
|
||||
} => {
|
||||
const isLoadingAuth = useAuth('useRBAC', (state) => state.isLoading);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<unknown>();
|
||||
const [data, setData] = useState<Record<string, boolean>>();
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState<unknown>();
|
||||
const [data, setData] = React.useState<Record<string, boolean>>();
|
||||
|
||||
const warnOnce = useMemo(() => once(console.warn), []);
|
||||
const warnOnce = React.useMemo(() => once(console.warn), []);
|
||||
|
||||
const actualPermissionsToCheck: Permission[] = useMemo(() => {
|
||||
const actualPermissionsToCheck: Permission[] = React.useMemo(() => {
|
||||
if (Array.isArray(permissionsToCheck)) {
|
||||
return permissionsToCheck;
|
||||
} else {
|
||||
@ -73,7 +74,7 @@ const useRBAC = (
|
||||
* This is the default value we return until the queryResults[i].data
|
||||
* are all resolved with data. This preserves the original behaviour.
|
||||
*/
|
||||
const defaultAllowedActions = useMemo(() => {
|
||||
const defaultAllowedActions = React.useMemo(() => {
|
||||
return actualPermissionsToCheck.reduce<Record<string, boolean>>((acc, permission) => {
|
||||
return {
|
||||
...acc,
|
||||
@ -85,13 +86,19 @@ const useRBAC = (
|
||||
const checkUserHasPermissions = useAuth('useRBAC', (state) => state.checkUserHasPermissions);
|
||||
|
||||
const permssionsChecked = usePrev(actualPermissionsToCheck);
|
||||
useEffect(() => {
|
||||
if (!isEqual(permssionsChecked, actualPermissionsToCheck)) {
|
||||
const contextChecked = usePrev(rawQueryContext);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
!isEqual(permssionsChecked, actualPermissionsToCheck) ||
|
||||
// TODO: also run this when the query context changes
|
||||
contextChecked !== rawQueryContext
|
||||
) {
|
||||
setIsLoading(true);
|
||||
setData(undefined);
|
||||
setError(undefined);
|
||||
|
||||
checkUserHasPermissions(actualPermissionsToCheck, passedPermissions)
|
||||
checkUserHasPermissions(actualPermissionsToCheck, passedPermissions, rawQueryContext)
|
||||
.then((res) => {
|
||||
if (res) {
|
||||
setData(
|
||||
@ -117,6 +124,8 @@ const useRBAC = (
|
||||
passedPermissions,
|
||||
permissionsToCheck,
|
||||
permssionsChecked,
|
||||
contextChecked,
|
||||
rawQueryContext,
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@ -1,6 +1,13 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { useRBAC, useAuth, type Permission, createContext, Page } from '@strapi/admin/strapi-admin';
|
||||
import {
|
||||
useRBAC,
|
||||
useAuth,
|
||||
type Permission,
|
||||
createContext,
|
||||
Page,
|
||||
useQueryParams,
|
||||
} from '@strapi/admin/strapi-admin';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import type { Schema } from '@strapi/types';
|
||||
@ -63,6 +70,7 @@ const DocumentRBAC = ({ children, permissions }: DocumentRBACProps) => {
|
||||
if (!slug) {
|
||||
throw new Error('Cannot find the slug param in the URL');
|
||||
}
|
||||
const [{ rawQuery }] = useQueryParams<{ plugins?: { i18n?: { locale?: string } } }>();
|
||||
|
||||
const userPermissions = useAuth('DocumentRBAC', (state) => state.permissions);
|
||||
|
||||
@ -76,7 +84,14 @@ const DocumentRBAC = ({ children, permissions }: DocumentRBACProps) => {
|
||||
}, {});
|
||||
}, [slug, userPermissions]);
|
||||
|
||||
const { isLoading, allowedActions } = useRBAC(contentTypePermissions, permissions ?? undefined);
|
||||
const { isLoading, allowedActions } = useRBAC(
|
||||
contentTypePermissions,
|
||||
permissions ?? undefined,
|
||||
// TODO: useRBAC context should be typed and built differently
|
||||
// We are passing raw query as context to the hook so that it can
|
||||
// rely on the locale provided from DocumentRBAC for its permission calculations.
|
||||
rawQuery
|
||||
);
|
||||
|
||||
const canCreateFields =
|
||||
!isLoading && allowedActions.canCreate
|
||||
|
||||
@ -195,16 +195,20 @@ const useDoc = () => {
|
||||
throw new Error('Could not find model in url params');
|
||||
}
|
||||
|
||||
const document = useDocument(
|
||||
{ documentId: origin || id, model: slug, collectionType, params },
|
||||
{
|
||||
skip: id === 'create' || (!origin && !id && collectionType !== SINGLE_TYPES),
|
||||
}
|
||||
);
|
||||
|
||||
const returnId = origin || id === 'create' ? undefined : id;
|
||||
|
||||
return {
|
||||
collectionType,
|
||||
model: slug,
|
||||
id: origin || id === 'create' ? undefined : id,
|
||||
...useDocument(
|
||||
{ documentId: origin || id, model: slug, collectionType, params },
|
||||
{
|
||||
skip: id === 'create' || (!origin && !id && collectionType !== SINGLE_TYPES),
|
||||
}
|
||||
),
|
||||
id: returnId,
|
||||
...document,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
} from '@strapi/admin/strapi-admin';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import { SINGLE_TYPES } from '../../../constants/collections';
|
||||
import { useDocumentRBAC } from '../../../features/DocumentRBAC';
|
||||
import { useDoc } from '../../../hooks/useDocument';
|
||||
import { useDocLayout } from '../../../hooks/useDocumentLayout';
|
||||
@ -35,7 +36,7 @@ type InputRendererProps = DistributiveOmit<EditFieldLayout, 'size'>;
|
||||
* components such as Blocks / Relations.
|
||||
*/
|
||||
const InputRenderer = ({ visible, hint: providedHint, ...props }: InputRendererProps) => {
|
||||
const { id } = useDoc();
|
||||
const { id, document, collectionType } = useDoc();
|
||||
const isFormDisabled = useForm('InputRenderer', (state) => state.disabled);
|
||||
|
||||
const isInDynamicZone = useDynamicZone('isInDynamicZone', (state) => state.isInDynamicZone);
|
||||
@ -45,8 +46,14 @@ const InputRenderer = ({ visible, hint: providedHint, ...props }: InputRendererP
|
||||
const canUpdateFields = useDocumentRBAC('InputRenderer', (rbac) => rbac.canUpdateFields);
|
||||
const canUserAction = useDocumentRBAC('InputRenderer', (rbac) => rbac.canUserAction);
|
||||
|
||||
const editableFields = id ? canUpdateFields : canCreateFields;
|
||||
const readableFields = id ? canReadFields : canCreateFields;
|
||||
let idToCheck = id;
|
||||
if (collectionType === SINGLE_TYPES) {
|
||||
idToCheck = document?.documentId;
|
||||
}
|
||||
|
||||
const editableFields = idToCheck ? canUpdateFields : canCreateFields;
|
||||
const readableFields = idToCheck ? canReadFields : canCreateFields;
|
||||
|
||||
/**
|
||||
* Component fields are always readable and editable,
|
||||
* however the fields within them may not be.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user