feat(authz): [FC-0099] create an custom error boundery (#11)
Create a custom error boundary for libraries team management workflow.
This commit is contained in:
1
.env
1
.env
@@ -1,6 +1,7 @@
|
|||||||
NODE_ENV='production'
|
NODE_ENV='production'
|
||||||
ACCESS_TOKEN_COOKIE_NAME=''
|
ACCESS_TOKEN_COOKIE_NAME=''
|
||||||
BASE_URL=''
|
BASE_URL=''
|
||||||
|
COURSE_AUTHORING_MICROFRONTEND_URL= ''
|
||||||
CREDENTIALS_BASE_URL=''
|
CREDENTIALS_BASE_URL=''
|
||||||
CSRF_TOKEN_API_PATH=''
|
CSRF_TOKEN_API_PATH=''
|
||||||
ECOMMERCE_BASE_URL=''
|
ECOMMERCE_BASE_URL=''
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ NODE_ENV='development'
|
|||||||
PORT=2025
|
PORT=2025
|
||||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||||
BASE_URL='http://localhost:2025'
|
BASE_URL='http://localhost:2025'
|
||||||
|
COURSE_AUTHORING_MICROFRONTEND_URL= 'http://apps.local.openedx.io:2001/authoring'
|
||||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||||
@@ -15,9 +16,10 @@ LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
|||||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||||
ORDER_HISTORY_URL='http://localhost:1996/orders'
|
ORDER_HISTORY_URL='http://localhost:1996/orders'
|
||||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
REFRESH_ACCESS_TOKEN_ENDPOINT='http://local.openedx.io:8000/login_refresh'
|
||||||
SEGMENT_KEY=''
|
SEGMENT_KEY=''
|
||||||
SITE_NAME=localhost
|
SITE_NAME=localhost
|
||||||
|
STUDIO_BASE_URL='http://studio.local.openedx.io:8001'
|
||||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||||
APP_ID='admin-console'
|
APP_ID='admin-console'
|
||||||
MFE_CONFIG_API_URL=''
|
MFE_CONFIG_API_URL=''
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -19,6 +19,7 @@
|
|||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-error-boundary": "^4.1.2",
|
||||||
"react-router-dom": "^6.0.0"
|
"react-router-dom": "^6.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -23798,7 +23799,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.1.2.tgz",
|
||||||
"integrity": "sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==",
|
"integrity": "sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5"
|
"@babel/runtime": "^7.12.5"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-error-boundary": "^4.1.2",
|
||||||
"react-router-dom": "^6.0.0"
|
"react-router-dom": "^6.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,23 +1,29 @@
|
|||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
import { Routes, Route } from 'react-router-dom';
|
import { Routes, Route } from 'react-router-dom';
|
||||||
import { ErrorBoundary } from '@edx/frontend-platform/react';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
|
import { QueryErrorResetBoundary } from '@tanstack/react-query';
|
||||||
import LoadingPage from '@src/components/LoadingPage';
|
import LoadingPage from '@src/components/LoadingPage';
|
||||||
|
import LibrariesErrorFallback from '@src/authz-module/libraries-manager/ErrorPage';
|
||||||
import { LibrariesTeamManager, LibrariesUserManager, LibrariesLayout } from './libraries-manager';
|
import { LibrariesTeamManager, LibrariesUserManager, LibrariesLayout } from './libraries-manager';
|
||||||
import { ROUTES } from './constants';
|
import { ROUTES } from './constants';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
const AuthZModule = () => (
|
const AuthZModule = () => (
|
||||||
<ErrorBoundary>
|
<QueryErrorResetBoundary>
|
||||||
<Suspense fallback={<LoadingPage />}>
|
{({ reset }) => (
|
||||||
<Routes>
|
<ErrorBoundary fallbackRender={LibrariesErrorFallback} onReset={reset}>
|
||||||
<Route element={<LibrariesLayout />}>
|
<Suspense fallback={<LoadingPage />}>
|
||||||
<Route path={ROUTES.LIBRARIES_TEAM_PATH} element={<LibrariesTeamManager />} />
|
<Routes>
|
||||||
<Route path={ROUTES.LIBRARIES_USER_PATH} element={<LibrariesUserManager />} />
|
<Route element={<LibrariesLayout />}>
|
||||||
</Route>
|
<Route path={ROUTES.LIBRARIES_TEAM_PATH} element={<LibrariesTeamManager />} />
|
||||||
</Routes>
|
<Route path={ROUTES.LIBRARIES_USER_PATH} element={<LibrariesUserManager />} />
|
||||||
</Suspense>
|
</Route>
|
||||||
</ErrorBoundary>
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
|
</ErrorBoundary>
|
||||||
|
)}
|
||||||
|
</QueryErrorResetBoundary>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default AuthZModule;
|
export default AuthZModule;
|
||||||
|
|||||||
73
src/authz-module/libraries-manager/ErrorPage/index.test.tsx
Normal file
73
src/authz-module/libraries-manager/ErrorPage/index.test.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { screen } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
|
import { renderWrapper } from '@src/setupTest';
|
||||||
|
import LibrariesErrorFallback from './index';
|
||||||
|
|
||||||
|
const ThrowError = ({ error }: { error:Error }) => {
|
||||||
|
throw error;
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('LibrariesErrorFallback', () => {
|
||||||
|
it('renders Access Denied for 401', () => {
|
||||||
|
const error = { name: '', message: 'NO_ACCESS', customAtributtes: { httpErrorStatus: 401 } };
|
||||||
|
renderWrapper(
|
||||||
|
<ErrorBoundary FallbackComponent={LibrariesErrorFallback}>
|
||||||
|
<ThrowError error={error} />
|
||||||
|
</ErrorBoundary>,
|
||||||
|
);
|
||||||
|
expect(screen.getByText(/Access Denied/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Back to Libraries/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders Not Found for 404', () => {
|
||||||
|
const error = { name: '', message: 'NOT_FOUND', customAtributtes: { httpErrorStatus: 404 } };
|
||||||
|
renderWrapper(
|
||||||
|
<ErrorBoundary FallbackComponent={LibrariesErrorFallback}>
|
||||||
|
<ThrowError error={error} />
|
||||||
|
</ErrorBoundary>,
|
||||||
|
);
|
||||||
|
expect(screen.getByText(/Page Not Found/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Back to Libraries/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders Server Error for 500 and shows reload', async () => {
|
||||||
|
const error = { name: '', message: 'SERVER_ERROR', customAtributtes: { httpErrorStatus: 500 } };
|
||||||
|
renderWrapper(
|
||||||
|
<ErrorBoundary FallbackComponent={LibrariesErrorFallback}>
|
||||||
|
<ThrowError error={error} />
|
||||||
|
</ErrorBoundary>,
|
||||||
|
);
|
||||||
|
expect(screen.getByText(/Something went wrong/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Reload Page/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Back to Libraries/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders generic error for other error error', () => {
|
||||||
|
const error = { name: '', message: 'SOMETHING_ELSE', customAtributtes: { httpErrorStatus: 418 } };
|
||||||
|
renderWrapper(
|
||||||
|
<ErrorBoundary FallbackComponent={LibrariesErrorFallback}>
|
||||||
|
<ThrowError error={error} />
|
||||||
|
</ErrorBoundary>,
|
||||||
|
);
|
||||||
|
expect(screen.getByText(/Error/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Back to Libraries/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls reload action if present', async () => {
|
||||||
|
// Simulate error with a refetch function
|
||||||
|
const refetch = jest.fn();
|
||||||
|
const error = {
|
||||||
|
name: '', message: 'SERVER_ERROR', customAtributtes: { httpErrorStatus: 500 }, refetch,
|
||||||
|
};
|
||||||
|
renderWrapper(
|
||||||
|
<ErrorBoundary FallbackComponent={LibrariesErrorFallback} onReset={refetch}>
|
||||||
|
<ThrowError error={error} />
|
||||||
|
</ErrorBoundary>,
|
||||||
|
);
|
||||||
|
const user = userEvent.setup();
|
||||||
|
await user.click(screen.getByText(/Reload Page/i));
|
||||||
|
expect(refetch).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
92
src/authz-module/libraries-manager/ErrorPage/index.tsx
Normal file
92
src/authz-module/libraries-manager/ErrorPage/index.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { FallbackProps } from 'react-error-boundary';
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
import {
|
||||||
|
Button, Container, Hyperlink, Row,
|
||||||
|
} from '@openedx/paragon';
|
||||||
|
import { CustomErrors, ERROR_STATUS } from '@src/constants';
|
||||||
|
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
const getErrorConfig = ({ errorMessage, errorStatus }) => {
|
||||||
|
if (errorMessage === CustomErrors.NO_ACCESS || ERROR_STATUS.NO_ACCESS.includes(errorStatus)) {
|
||||||
|
return ({
|
||||||
|
title: messages['error.page.title.noAccess'],
|
||||||
|
description: messages['error.page.message.noAccess'],
|
||||||
|
statusCode: errorStatus || ERROR_STATUS.NO_ACCESS[0],
|
||||||
|
showBackButton: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (errorMessage === CustomErrors.NOT_FOUND || ERROR_STATUS.NOT_FOUND.includes(errorStatus)) {
|
||||||
|
return ({
|
||||||
|
title: messages['error.page.title.notFound'],
|
||||||
|
description: messages['error.page.message.notFound'],
|
||||||
|
statusCode: errorStatus || ERROR_STATUS.NOT_FOUND[0],
|
||||||
|
showBackButton: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (errorMessage === CustomErrors.SERVER_ERROR || ERROR_STATUS.SERVER_ERROR.includes(errorStatus)) {
|
||||||
|
return ({
|
||||||
|
title: messages['error.page.title.server'],
|
||||||
|
description: messages['error.page.message.server'],
|
||||||
|
statusCode: errorStatus || ERROR_STATUS.SERVER_ERROR[0],
|
||||||
|
showBackButton: true,
|
||||||
|
showReloadButton: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return ({
|
||||||
|
title: messages['error.page.title.generic'],
|
||||||
|
description: messages['error.page.message.generic'],
|
||||||
|
showBackButton: true,
|
||||||
|
showReloadButton: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const ErrorPage = ({ error, resetErrorBoundary }: FallbackProps) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const [reloading, setReloading] = useState(false);
|
||||||
|
|
||||||
|
const errorStatus: number = error?.customAttributes?.httpErrorStatus;
|
||||||
|
const errorMessage: string = error?.message;
|
||||||
|
const {
|
||||||
|
title, description, statusCode, showBackButton, showReloadButton,
|
||||||
|
} = getErrorConfig({ errorMessage, errorStatus });
|
||||||
|
|
||||||
|
const handleReload = () => {
|
||||||
|
setReloading(true);
|
||||||
|
resetErrorBoundary();
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Container className="d-flex flex-column align-items-center justify-content-center min-vh-100 bg-light-200">
|
||||||
|
<h1 className="display-4 text-primary-200">{statusCode}</h1>
|
||||||
|
<h1 className="text-primary">{intl.formatMessage(title)}</h1>
|
||||||
|
<p>{intl.formatMessage(description)}</p>
|
||||||
|
<Row>
|
||||||
|
{showReloadButton && (
|
||||||
|
<Button
|
||||||
|
className="m-2"
|
||||||
|
disabled={reloading}
|
||||||
|
onClick={handleReload}
|
||||||
|
>
|
||||||
|
{intl.formatMessage(messages['error.page.action.reload'])}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{showBackButton && (
|
||||||
|
<Button
|
||||||
|
as={Hyperlink}
|
||||||
|
destination={`${getConfig().COURSE_AUTHORING_MICROFRONTEND_URL}/libraries`}
|
||||||
|
className="m-2"
|
||||||
|
variant={showReloadButton ? 'outline-primary' : 'primary'}
|
||||||
|
>
|
||||||
|
{intl.formatMessage(messages['error.page.action.back'])}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const LibrariesErrorFallback = (props: FallbackProps) => <ErrorPage {...props} />;
|
||||||
|
export default LibrariesErrorFallback;
|
||||||
56
src/authz-module/libraries-manager/ErrorPage/messages.ts
Normal file
56
src/authz-module/libraries-manager/ErrorPage/messages.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
'error.page.title.noAccess': {
|
||||||
|
id: 'error.page.tile.noAccess',
|
||||||
|
defaultMessage: 'Access Denied',
|
||||||
|
description: 'Title error when user does not have access to view',
|
||||||
|
},
|
||||||
|
'error.page.message.noAccess': {
|
||||||
|
id: 'error.page.message.noAccess',
|
||||||
|
defaultMessage: 'You do not have permission to view this page.',
|
||||||
|
description: 'Error message when user does not have access to view',
|
||||||
|
},
|
||||||
|
'error.page.title.notFound': {
|
||||||
|
id: 'error.page.tile.notFound',
|
||||||
|
defaultMessage: 'Page Not Found',
|
||||||
|
description: 'Error the resource is not found',
|
||||||
|
},
|
||||||
|
'error.page.message.notFound': {
|
||||||
|
id: 'error.page.message.notFound',
|
||||||
|
defaultMessage: 'The library you are looking for could not be found.',
|
||||||
|
description: 'Error message when the resource is not found',
|
||||||
|
},
|
||||||
|
'error.page.title.server': {
|
||||||
|
id: 'error.page.tile.server',
|
||||||
|
defaultMessage: 'Something went wrong',
|
||||||
|
description: 'Title for server error',
|
||||||
|
},
|
||||||
|
'error.page.message.server': {
|
||||||
|
id: 'error.page.message.server.error',
|
||||||
|
defaultMessage: 'We\'re experiencing an internal server problem. Please try again later',
|
||||||
|
description: 'Server error message for unexpected errors',
|
||||||
|
},
|
||||||
|
'error.page.title.generic': {
|
||||||
|
id: 'error.page.tile.generic',
|
||||||
|
defaultMessage: 'Something went wrong',
|
||||||
|
description: 'Title for unexpected error',
|
||||||
|
},
|
||||||
|
'error.page.message.generic': {
|
||||||
|
id: 'error.page.message.server',
|
||||||
|
defaultMessage: 'An unexpected error occurred. Please click the button below to refresh the page.',
|
||||||
|
description: 'Error message for unexpected errors',
|
||||||
|
},
|
||||||
|
'error.page.action.reload': {
|
||||||
|
id: 'error.page.action.reload',
|
||||||
|
defaultMessage: 'Reload Page',
|
||||||
|
description: 'Label for reload action',
|
||||||
|
},
|
||||||
|
'error.page.action.back': {
|
||||||
|
id: 'error.page.action.back',
|
||||||
|
defaultMessage: 'Back to Libraries',
|
||||||
|
description: 'Label for return to libraries action',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default messages;
|
||||||
@@ -4,6 +4,7 @@ import { useParams } from 'react-router-dom';
|
|||||||
import { useValidateUserPermissions } from '@src/data/hooks';
|
import { useValidateUserPermissions } from '@src/data/hooks';
|
||||||
import { renderWrapper } from '@src/setupTest';
|
import { renderWrapper } from '@src/setupTest';
|
||||||
import { usePermissionsByRole } from '@src/authz-module/data/hooks';
|
import { usePermissionsByRole } from '@src/authz-module/data/hooks';
|
||||||
|
import { CustomErrors } from '@src/constants';
|
||||||
import { LibraryAuthZProvider, useLibraryAuthZ } from './context';
|
import { LibraryAuthZProvider, useLibraryAuthZ } from './context';
|
||||||
|
|
||||||
jest.mock('react-router-dom', () => ({
|
jest.mock('react-router-dom', () => ({
|
||||||
@@ -130,7 +131,7 @@ describe('LibraryAuthZProvider', () => {
|
|||||||
<TestComponent />
|
<TestComponent />
|
||||||
</LibraryAuthZProvider>,
|
</LibraryAuthZProvider>,
|
||||||
);
|
);
|
||||||
}).toThrow('NoAccess');
|
}).toThrow(CustomErrors.NO_ACCESS);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('provides context when user can view but not manage team', () => {
|
it('provides context when user can view but not manage team', () => {
|
||||||
@@ -161,7 +162,7 @@ describe('LibraryAuthZProvider', () => {
|
|||||||
</LibraryAuthZProvider>
|
</LibraryAuthZProvider>
|
||||||
</ErrorBoundary>,
|
</ErrorBoundary>,
|
||||||
);
|
);
|
||||||
}).toThrow('MissingLibrary');
|
}).toThrow(CustomErrors.NOT_FOUND);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws error when useLibraryAuthZ is used outside provider', () => {
|
it('throws error when useLibraryAuthZ is used outside provider', () => {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { AppContext } from '@edx/frontend-platform/react';
|
|||||||
import { useValidateUserPermissions } from '@src/data/hooks';
|
import { useValidateUserPermissions } from '@src/data/hooks';
|
||||||
import { usePermissionsByRole } from '@src/authz-module/data/hooks';
|
import { usePermissionsByRole } from '@src/authz-module/data/hooks';
|
||||||
import { PermissionMetadata, ResourceMetadata, Role } from 'types';
|
import { PermissionMetadata, ResourceMetadata, Role } from 'types';
|
||||||
|
import { CustomErrors } from '@src/constants';
|
||||||
import { libraryPermissions, libraryResourceTypes, libraryRolesMetadata } from './constants';
|
import { libraryPermissions, libraryResourceTypes, libraryRolesMetadata } from './constants';
|
||||||
|
|
||||||
const LIBRARY_TEAM_PERMISSIONS = ['view_library_team', 'manage_library_team'];
|
const LIBRARY_TEAM_PERMISSIONS = ['view_library_team', 'manage_library_team'];
|
||||||
@@ -38,7 +39,7 @@ export const LibraryAuthZProvider: React.FC<AuthZProviderProps> = ({ children }:
|
|||||||
|
|
||||||
// TODO: Implement a custom error view
|
// TODO: Implement a custom error view
|
||||||
if (!libraryId) {
|
if (!libraryId) {
|
||||||
throw new Error('MissingLibrary');
|
throw new Error(CustomErrors.NOT_FOUND);
|
||||||
}
|
}
|
||||||
const permissions = LIBRARY_TEAM_PERMISSIONS.map(action => ({ action, scope: libraryId }));
|
const permissions = LIBRARY_TEAM_PERMISSIONS.map(action => ({ action, scope: libraryId }));
|
||||||
|
|
||||||
@@ -46,7 +47,7 @@ export const LibraryAuthZProvider: React.FC<AuthZProviderProps> = ({ children }:
|
|||||||
const [{ allowed: canViewTeam }, { allowed: canManageTeam }] = userPermissions;
|
const [{ allowed: canViewTeam }, { allowed: canManageTeam }] = userPermissions;
|
||||||
|
|
||||||
if (!canViewTeam && !canManageTeam) {
|
if (!canViewTeam && !canManageTeam) {
|
||||||
throw new Error('NoAccess');
|
throw new Error(CustomErrors.NO_ACCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: libraryRoles } = usePermissionsByRole(libraryId);
|
const { data: libraryRoles } = usePermissionsByRole(libraryId);
|
||||||
|
|||||||
@@ -1 +1,17 @@
|
|||||||
export const appId = 'org.openedx.frontend.app.adminConsole';
|
export const appId = 'org.openedx.frontend.app.adminConsole';
|
||||||
|
|
||||||
|
export enum CustomErrors {
|
||||||
|
NO_ACCESS = 'NO_ACCESS',
|
||||||
|
NOT_FOUND = 'NOT_FOUND',
|
||||||
|
SERVER_ERROR = 'SERVER_ERROR',
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorStatusCode = {
|
||||||
|
[key in CustomErrors]: number[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ERROR_STATUS: ErrorStatusCode = {
|
||||||
|
[CustomErrors.NO_ACCESS]: [403, 401],
|
||||||
|
[CustomErrors.NOT_FOUND]: [404],
|
||||||
|
[CustomErrors.SERVER_ERROR]: [500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511],
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Route, Routes } from 'react-router-dom';
|
|||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { AppProvider, ErrorPage } from '@edx/frontend-platform/react';
|
import { AppProvider, ErrorPage } from '@edx/frontend-platform/react';
|
||||||
import {
|
import {
|
||||||
APP_INIT_ERROR, APP_READY, subscribe, initialize,
|
APP_INIT_ERROR, APP_READY, subscribe, initialize, mergeConfig,
|
||||||
} from '@edx/frontend-platform';
|
} from '@edx/frontend-platform';
|
||||||
import AuthZModule from 'authz-module';
|
import AuthZModule from 'authz-module';
|
||||||
|
|
||||||
@@ -43,4 +43,11 @@ subscribe(APP_INIT_ERROR, (error) => {
|
|||||||
initialize({
|
initialize({
|
||||||
messages,
|
messages,
|
||||||
requireAuthenticatedUser: true,
|
requireAuthenticatedUser: true,
|
||||||
|
handlers: {
|
||||||
|
config: () => {
|
||||||
|
mergeConfig({
|
||||||
|
COURSE_AUTHORING_MICROFRONTEND_URL: process.env.COURSE_AUTHORING_MICROFRONTEND_URL || null,
|
||||||
|
}, 'AdminConsoleAppConfig');
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user