diff --git a/packages/core/admin/admin/src/contexts/apiTokenPermissions.tsx b/packages/core/admin/admin/src/contexts/apiTokenPermissions.tsx
index 733b96ce60..e9341f89b5 100644
--- a/packages/core/admin/admin/src/contexts/apiTokenPermissions.tsx
+++ b/packages/core/admin/admin/src/contexts/apiTokenPermissions.tsx
@@ -3,31 +3,35 @@
import * as React from 'react';
import { createContext } from '@radix-ui/react-context';
-import { Entity } from '@strapi/types';
-interface PseudoEvent {
+import { List as ListContentApiPermissions } from '../../../shared/contracts/content-api/permissions';
+import { List as ListContentApiRoutes } from '../../../shared/contracts/content-api/routes';
+
+export interface PseudoEvent {
target: { value: string };
}
interface ApiTokenPermissionsContextValue {
- selectedAction: string[] | null;
- routes: string[];
- selectedActions: string[];
- data: {
- allActionsIds: Entity.ID[];
- permissions: {
- apiId: string;
- label: string;
- controllers: { controller: string; actions: { actionId: string; action: string } }[];
- }[];
+ value: {
+ selectedAction: string | null;
+ routes: ListContentApiRoutes.Response['data'];
+ selectedActions: string[];
+ data: {
+ allActionsIds: string[];
+ permissions: ListContentApiPermissions.Response['data'][];
+ };
+ onChange: ({ target: { value } }: PseudoEvent) => void;
+ onChangeSelectAll: ({
+ target: { value },
+ }: {
+ target: { value: { action: string; actionId: string }[] };
+ }) => void;
+ setSelectedAction: ({ target: { value } }: PseudoEvent) => void;
};
- onChange: ({ target: { value } }: PseudoEvent) => void;
- onChangeSelectAll: ({ target: { value } }: PseudoEvent) => void;
- setSelectedAction: ({ target: { value } }: PseudoEvent) => void;
}
interface ApiTokenPermissionsContextProviderProps extends ApiTokenPermissionsContextValue {
- children: React.ReactNode[];
+ children: React.ReactNode | React.ReactNode[];
}
const [ApiTokenPermissionsContextProvider, useApiTokenPermissionsContext] =
diff --git a/packages/core/admin/admin/src/pages/Settings/components/Tokens/LifeSpanInput/index.js b/packages/core/admin/admin/src/pages/Settings/components/Tokens/LifeSpanInput/index.js
index 72b901ddb8..2ac9fcb9ca 100644
--- a/packages/core/admin/admin/src/pages/Settings/components/Tokens/LifeSpanInput/index.js
+++ b/packages/core/admin/admin/src/pages/Settings/components/Tokens/LifeSpanInput/index.js
@@ -4,7 +4,7 @@ import { Option, Select, Typography } from '@strapi/design-system';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
-import { getDateOfExpiration } from '../../../pages/ApiTokens/EditView/utils';
+import { getDateOfExpiration } from '../../../pages/ApiTokens/EditView/utils/getDateOfExpiration';
const LifeSpanInput = ({ token, errors, values, onChange, isCreating }) => {
const { formatMessage } = useIntl();
diff --git a/packages/core/admin/admin/src/pages/Settings/components/Tokens/constants.js b/packages/core/admin/admin/src/pages/Settings/components/Tokens/constants.ts
similarity index 100%
rename from packages/core/admin/admin/src/pages/Settings/components/Tokens/constants.js
rename to packages/core/admin/admin/src/pages/Settings/components/Tokens/constants.ts
diff --git a/packages/core/admin/admin/src/pages/Settings/constants.ts b/packages/core/admin/admin/src/pages/Settings/constants.ts
index 87fa7331ba..1fc73ec898 100644
--- a/packages/core/admin/admin/src/pages/Settings/constants.ts
+++ b/packages/core/admin/admin/src/pages/Settings/constants.ts
@@ -88,30 +88,27 @@ export const ROUTES_CE: Route[] = [
},
{
async Component() {
- // @ts-expect-error – No types, yet.
- const component = await import('./pages/ApiTokens/ProtectedListView');
+ const { ProtectedListView } = await import('./pages/ApiTokens/ListView');
- return component;
+ return ProtectedListView;
},
to: '/settings/api-tokens',
exact: true,
},
{
async Component() {
- // @ts-expect-error – No types, yet.
- const component = await import('./pages/ApiTokens/ProtectedCreateView');
+ const { ProtectedCreateView } = await import('./pages/ApiTokens/CreateView');
- return component;
+ return ProtectedCreateView;
},
to: '/settings/api-tokens/create',
exact: true,
},
{
async Component() {
- // @ts-expect-error – No types, yet.
- const component = await import('./pages/ApiTokens/ProtectedEditView');
+ const { ProtectedEditView } = await import('./pages/ApiTokens/EditView/EditViewPage');
- return component;
+ return ProtectedEditView;
},
to: '/settings/api-tokens/:id',
exact: true,
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/CreateView.tsx b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/CreateView.tsx
new file mode 100644
index 0000000000..781a852d28
--- /dev/null
+++ b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/CreateView.tsx
@@ -0,0 +1,16 @@
+import { CheckPagePermissions } from '@strapi/helper-plugin';
+import { useSelector } from 'react-redux';
+
+import { selectAdminPermissions } from '../../../../selectors';
+
+import { EditView } from './EditView/EditViewPage';
+
+export const ProtectedCreateView = () => {
+ const permissions = useSelector(selectAdminPermissions);
+
+ return (
+
+
+
+ );
+};
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/index.js b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/EditViewPage.tsx
similarity index 56%
rename from packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/index.js
rename to packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/EditViewPage.tsx
index d447c75323..5107750e83 100644
--- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/index.js
+++ b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/EditViewPage.tsx
@@ -1,7 +1,8 @@
-import React, { useEffect, useReducer, useRef, useState } from 'react';
+import * as React from 'react';
import { ContentLayout, Flex, Main } from '@strapi/design-system';
import {
+ CheckPagePermissions,
Form,
SettingsPageTitle,
useFetchClient,
@@ -12,77 +13,100 @@ import {
useRBAC,
useTracking,
} from '@strapi/helper-plugin';
-import { Formik } from 'formik';
+import { AxiosError, AxiosResponse } from 'axios';
+import { Formik, FormikHelpers } from 'formik';
import { useIntl } from 'react-intl';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
-import { useHistory, useRouteMatch } from 'react-router-dom';
+import { useLocation, useHistory, useRouteMatch } from 'react-router-dom';
-import { ApiTokenPermissionsProvider } from '../../../../../contexts/apiTokenPermissions';
+import {
+ ApiTokenPermissionsContextValue,
+ ApiTokenPermissionsProvider,
+} from '../../../../../contexts/apiTokenPermissions';
import { selectAdminPermissions } from '../../../../../selectors';
import { formatAPIErrors } from '../../../../../utils/formatAPIErrors';
import { API_TOKEN_TYPE } from '../../../components/Tokens/constants';
+// @ts-expect-error not converted yet
import FormHead from '../../../components/Tokens/FormHead';
+// @ts-expect-error not converted yet
import TokenBox from '../../../components/Tokens/TokenBox';
-import FormApiTokenContainer from './components/FormApiTokenContainer';
-import LoadingView from './components/LoadingView';
-import Permissions from './components/Permissions';
-import init from './init';
-import reducer, { initialState } from './reducer';
-import { schema } from './utils';
+import { FormApiTokenContainer } from './components/FormApiTokenContainer';
+import { LoadingView } from './components/LoadingView';
+import { Permissions } from './components/Permissions';
+import { schema } from './constants';
+import { initialState, reducer } from './reducer';
+
+import type {
+ Get,
+ Update,
+ Create,
+ ApiToken,
+} from '../../../../../../../shared/contracts/api-token';
+import type { List as ListContentApiPermissions } from '../../../../../../../shared/contracts/content-api/permissions';
+import type { List as ListContentApiRoutes } from '../../../../../../../shared/contracts/content-api/routes';
const MSG_ERROR_NAME_TAKEN = 'Name already taken';
-const ApiTokenCreateView = () => {
+export const EditView = () => {
useFocusWhenNavigate();
const { formatMessage } = useIntl();
const { lockApp, unlockApp } = useOverlayBlocker();
const toggleNotification = useNotification();
- const history = useHistory();
+ const { state: locationState } = useLocation<{ apiToken: ApiToken }>();
const permissions = useSelector(selectAdminPermissions);
- const [apiToken, setApiToken] = useState(
- history.location.state?.apiToken.accessKey
+ const [apiToken, setApiToken] = React.useState(
+ locationState?.apiToken?.accessKey
? {
- ...history.location.state.apiToken,
+ ...locationState.apiToken,
}
: null
);
const { trackUsage } = useTracking();
- const trackUsageRef = useRef(trackUsage);
const { setCurrentStep } = useGuidedTour();
const {
allowedActions: { canCreate, canUpdate, canRegenerate },
- } = useRBAC(permissions.settings['api-tokens']);
- const [state, dispatch] = useReducer(reducer, initialState, (state) => init(state, {}));
- const {
- params: { id },
- } = useRouteMatch('/settings/api-tokens/:id');
+ } = useRBAC(permissions.settings?.['api-tokens']);
+ const [state, dispatch] = React.useReducer(reducer, initialState);
+ const match = useRouteMatch<{ id: string }>('/settings/api-tokens/:id');
+ const id = match?.params?.id;
const { get, post, put } = useFetchClient();
+ const history = useHistory();
const isCreating = id === 'create';
useQuery(
'content-api-permissions',
async () => {
- const [permissions, routes] = await Promise.all(
+ await Promise.all(
['/admin/content-api/permissions', '/admin/content-api/routes'].map(async (url) => {
- const { data } = await get(url);
+ if (url === '/admin/content-api/permissions') {
+ const {
+ data: { data },
+ } = await get(url);
- return data.data;
+ dispatch({
+ type: 'UPDATE_PERMISSIONS_LAYOUT',
+ value: data,
+ });
+
+ return data;
+ } else if (url === '/admin/content-api/routes') {
+ const {
+ data: { data },
+ } = await get(url);
+
+ dispatch({
+ type: 'UPDATE_ROUTES',
+ value: data,
+ });
+
+ return data;
+ }
})
);
- dispatch({
- type: 'UPDATE_PERMISSIONS_LAYOUT',
- value: permissions,
- });
-
- dispatch({
- type: 'UPDATE_ROUTES',
- value: routes,
- });
-
if (apiToken) {
if (apiToken?.type === 'read-only') {
dispatch({
@@ -112,18 +136,18 @@ const ApiTokenCreateView = () => {
}
);
- useEffect(() => {
- trackUsageRef.current(isCreating ? 'didAddTokenFromList' : 'didEditTokenFromList', {
+ React.useEffect(() => {
+ trackUsage(isCreating ? 'didAddTokenFromList' : 'didEditTokenFromList', {
tokenType: API_TOKEN_TYPE,
});
- }, [isCreating]);
+ }, [isCreating, trackUsage]);
const { status } = useQuery(
['api-token', id],
async () => {
const {
data: { data },
- } = await get(`/admin/api-tokens/${id}`);
+ } = await get(`/admin/api-tokens/${id}`);
setApiToken({
...data,
@@ -159,37 +183,56 @@ const ApiTokenCreateView = () => {
}
);
- const handleSubmit = async (body, actions) => {
- trackUsageRef.current(isCreating ? 'willCreateToken' : 'willEditToken', {
+ const handleSubmit = async (
+ body: Pick & {
+ lifespan: Get.Response['data']['lifespan'] | undefined;
+ type: Get.Response['data']['type'] | undefined;
+ },
+ actions: FormikHelpers<
+ Pick & {
+ lifespan: Get.Response['data']['lifespan'] | undefined;
+ type: Get.Response['data']['type'] | undefined;
+ }
+ >
+ ) => {
+ trackUsage(isCreating ? 'willCreateToken' : 'willEditToken', {
tokenType: API_TOKEN_TYPE,
});
+
+ // @ts-expect-error context assertation
lockApp();
- const lifespanVal =
- body.lifespan && parseInt(body.lifespan, 10) && body.lifespan !== '0'
- ? parseInt(body.lifespan, 10)
- : null;
try {
const {
data: { data: response },
} = isCreating
- ? await post(`/admin/api-tokens`, {
- ...body,
- lifespan: lifespanVal,
- permissions: body.type === 'custom' ? state.selectedActions : null,
- })
- : await put(`/admin/api-tokens/${id}`, {
- name: body.name,
- description: body.description,
- type: body.type,
- permissions: body.type === 'custom' ? state.selectedActions : null,
- });
+ ? await post, Create.Request['body']>(
+ `/admin/api-tokens`,
+ {
+ ...body,
+ // in case a token has a lifespan of "unlimited" the API only accepts zero as a number
+ lifespan: body.lifespan === '0' ? parseInt(body.lifespan) : null,
+ permissions: body.type === 'custom' ? state.selectedActions : null,
+ }
+ )
+ : await put, Update.Request['body']>(
+ `/admin/api-tokens/${id}`,
+ {
+ name: body.name,
+ description: body.description,
+ type: body.type,
+ permissions: body.type === 'custom' ? state.selectedActions : null,
+ }
+ );
if (isCreating) {
history.replace(`/settings/api-tokens/${response.id}`, { apiToken: response });
setCurrentStep('apiTokens.success');
}
+
+ // @ts-expect-error context assertation
unlockApp();
+
setApiToken({
...response,
});
@@ -207,32 +250,40 @@ const ApiTokenCreateView = () => {
}),
});
- trackUsageRef.current(isCreating ? 'didCreateToken' : 'didEditToken', {
- type: apiToken.type,
- tokenType: API_TOKEN_TYPE,
- });
- } catch (err) {
- const errors = formatAPIErrors(err.response.data);
- actions.setErrors(errors);
-
- if (err?.response?.data?.error?.message === MSG_ERROR_NAME_TAKEN) {
- toggleNotification({
- type: 'warning',
- message: err.response.data.message || 'notification.error.tokennamenotunique',
- });
- } else {
- toggleNotification({
- type: 'warning',
- message: err?.response?.data?.message || 'notification.error',
+ if (apiToken?.type) {
+ trackUsage(isCreating ? 'didCreateToken' : 'didEditToken', {
+ type: apiToken.type,
+ tokenType: API_TOKEN_TYPE,
});
}
+ } catch (err) {
+ if (err instanceof AxiosError && err.response) {
+ const errors = formatAPIErrors(err.response.data);
+ actions.setErrors(errors);
+
+ if (err?.response?.data?.error?.message === MSG_ERROR_NAME_TAKEN) {
+ toggleNotification({
+ type: 'warning',
+ message: err.response.data.message || 'notification.error.tokennamenotunique',
+ });
+ } else {
+ toggleNotification({
+ type: 'warning',
+ message: err?.response?.data?.message || 'notification.error',
+ });
+ }
+ }
+
+ // @ts-expect-error context assertation
unlockApp();
}
};
- const [hasChangedPermissions, setHasChangedPermissions] = useState(false);
+ const [hasChangedPermissions, setHasChangedPermissions] = React.useState(false);
- const handleChangeCheckbox = ({ target: { value } }) => {
+ const handleChangeCheckbox = ({
+ target: { value },
+ }: Parameters[0]) => {
setHasChangedPermissions(true);
dispatch({
type: 'ON_CHANGE',
@@ -240,7 +291,9 @@ const ApiTokenCreateView = () => {
});
};
- const handleChangeSelectAllCheckbox = ({ target: { value } }) => {
+ const handleChangeSelectAllCheckbox = ({
+ target: { value },
+ }: Parameters[0]) => {
setHasChangedPermissions(true);
dispatch({
type: 'SELECT_ALL_IN_PERMISSION',
@@ -248,7 +301,9 @@ const ApiTokenCreateView = () => {
});
};
- const setSelectedAction = ({ target: { value } }) => {
+ const setSelectedAction = ({
+ target: { value },
+ }: Parameters[0]) => {
dispatch({
type: 'SET_SELECTED_ACTION',
value,
@@ -266,6 +321,7 @@ const ApiTokenCreateView = () => {
const isLoading = !isCreating && !apiToken && status !== 'success';
if (isLoading) {
+ // @ts-expect-error this is probably fine for now
return ;
}
@@ -339,4 +395,12 @@ const ApiTokenCreateView = () => {
);
};
-export default ApiTokenCreateView;
+export const ProtectedEditView = () => {
+ const permissions = useSelector(selectAdminPermissions);
+
+ return (
+
+
+
+ );
+};
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/ActionBoundRoutes/index.js b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/ActionBoundRoutes.tsx
similarity index 71%
rename from packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/ActionBoundRoutes/index.js
rename to packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/ActionBoundRoutes.tsx
index 1958338994..a81321b301 100644
--- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/ActionBoundRoutes/index.js
+++ b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/ActionBoundRoutes.tsx
@@ -1,12 +1,11 @@
-import React from 'react';
-
import { Flex, GridItem, Typography } from '@strapi/design-system';
import { useIntl } from 'react-intl';
-import { useApiTokenPermissions } from '../../../../../../../contexts/apiTokenPermissions';
-import BoundRoute from '../BoundRoute';
+import { useApiTokenPermissions } from '../../../../../../contexts/apiTokenPermissions';
-const ActionBoundRoutes = () => {
+import { BoundRoute } from './BoundRoute';
+
+export const ActionBoundRoutes = () => {
const {
value: { selectedAction, routes },
} = useApiTokenPermissions();
@@ -25,12 +24,14 @@ const ActionBoundRoutes = () => {
>
{selectedAction ? (
- {routes[actionSection]?.map((route) => {
- return route.config.auth?.scope?.includes(selectedAction) ||
- route.handler === selectedAction ? (
-
- ) : null;
- })}
+ {actionSection &&
+ actionSection in routes &&
+ routes[actionSection].map((route) => {
+ return route.config.auth?.scope?.includes(selectedAction) ||
+ route.handler === selectedAction ? (
+
+ ) : null;
+ })}
) : (
@@ -52,5 +53,3 @@ const ActionBoundRoutes = () => {
);
};
-
-export default ActionBoundRoutes;
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/BoundRoute/index.js b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/BoundRoute.tsx
similarity index 59%
rename from packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/BoundRoute/index.js
rename to packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/BoundRoute.tsx
index 0f3467bc5a..14d456b508 100644
--- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/BoundRoute/index.js
+++ b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/BoundRoute.tsx
@@ -3,18 +3,77 @@ import React from 'react';
import { Box, Flex, Typography } from '@strapi/design-system';
import map from 'lodash/map';
import tail from 'lodash/tail';
-import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
-import styled from 'styled-components';
+import styled, { DefaultTheme } from 'styled-components';
-import getMethodColor from './getMethodColor';
+type HttpVerb = 'POST' | 'GET' | 'PUT' | 'DELETE';
+
+type MethodColor = {
+ text: keyof DefaultTheme['colors'];
+ border: keyof DefaultTheme['colors'];
+ background: keyof DefaultTheme['colors'];
+};
+
+const getMethodColor = (verb: HttpVerb): MethodColor => {
+ switch (verb) {
+ case 'POST': {
+ return {
+ text: 'success600',
+ border: 'success200',
+ background: 'success100',
+ };
+ }
+ case 'GET': {
+ return {
+ text: 'secondary600',
+ border: 'secondary200',
+ background: 'secondary100',
+ };
+ }
+ case 'PUT': {
+ return {
+ text: 'warning600',
+ border: 'warning200',
+ background: 'warning100',
+ };
+ }
+ case 'DELETE': {
+ return {
+ text: 'danger600',
+ border: 'danger200',
+ background: 'danger100',
+ };
+ }
+ default: {
+ return {
+ text: 'neutral600',
+ border: 'neutral200',
+ background: 'neutral100',
+ };
+ }
+ }
+};
const MethodBox = styled(Box)`
margin: -1px;
border-radius: ${({ theme }) => theme.spaces[1]} 0 0 ${({ theme }) => theme.spaces[1]};
`;
-function BoundRoute({ route }) {
+interface BoundRouteProps {
+ route: {
+ handler: string;
+ method: HttpVerb;
+ path: string;
+ };
+}
+
+export const BoundRoute = ({
+ route = {
+ handler: 'Nocontroller.error',
+ method: 'GET',
+ path: '/there-is-no-path',
+ },
+}: BoundRouteProps) => {
const { formatMessage } = useIntl();
const { method, handler: title, path } = route;
@@ -51,22 +110,4 @@ function BoundRoute({ route }) {
);
-}
-
-BoundRoute.defaultProps = {
- route: {
- handler: 'Nocontroller.error',
- method: 'GET',
- path: '/there-is-no-path',
- },
};
-
-BoundRoute.propTypes = {
- route: PropTypes.shape({
- handler: PropTypes.string,
- method: PropTypes.string,
- path: PropTypes.string,
- }),
-};
-
-export default BoundRoute;
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/BoundRoute/getMethodColor.js b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/BoundRoute/getMethodColor.js
deleted file mode 100644
index 1ad903b389..0000000000
--- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/BoundRoute/getMethodColor.js
+++ /dev/null
@@ -1,41 +0,0 @@
-const getMethodColor = (verb) => {
- switch (verb) {
- case 'POST': {
- return {
- text: 'success600',
- border: 'success200',
- background: 'success100',
- };
- }
- case 'GET': {
- return {
- text: 'secondary600',
- border: 'secondary200',
- background: 'secondary100',
- };
- }
- case 'PUT': {
- return {
- text: 'warning600',
- border: 'warning200',
- background: 'warning100',
- };
- }
- case 'DELETE': {
- return {
- text: 'danger600',
- border: 'danger200',
- background: 'danger100',
- };
- }
- default: {
- return {
- text: 'neutral600',
- border: 'neutral200',
- background: 'neutral100',
- };
- }
- }
-};
-
-export default getMethodColor;
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/CollapsableContentType/index.js b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/CollabsableContentType.tsx
similarity index 73%
rename from packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/CollapsableContentType/index.js
rename to packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/CollabsableContentType.tsx
index 139e577fe6..83d0be4d5d 100644
--- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/CollapsableContentType/index.js
+++ b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/CollabsableContentType.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react';
+import * as React from 'react';
import {
Accordion,
@@ -13,13 +13,37 @@ import {
} from '@strapi/design-system';
import { Cog } from '@strapi/icons';
import capitalize from 'lodash/capitalize';
-import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
-import styled from 'styled-components';
+import styled, { css } from 'styled-components';
-import { useApiTokenPermissions } from '../../../../../../../contexts/apiTokenPermissions';
+import { ContentApiPermission } from '../../../../../../../../shared/contracts/content-api/permissions';
+import { useApiTokenPermissions } from '../../../../../../contexts/apiTokenPermissions';
-import CheckboxWrapper from './CheckBoxWrapper';
+const activeCheckboxWrapperStyles = css`
+ background: ${(props) => props.theme.colors.primary100};
+ svg {
+ opacity: 1;
+ }
+`;
+
+const CheckboxWrapper = styled(Box)<{ isActive: boolean }>`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ svg {
+ opacity: 0;
+ path {
+ fill: ${(props) => props.theme.colors.primary600};
+ }
+ }
+
+ /* Show active style both on hover and when the action is selected */
+ ${(props) => props.isActive && activeCheckboxWrapperStyles}
+ &:hover {
+ ${activeCheckboxWrapperStyles}
+ }
+`;
const Border = styled.div`
flex: 1;
@@ -27,18 +51,27 @@ const Border = styled.div`
border-top: 1px solid ${({ theme }) => theme.colors.neutral150};
`;
-const CollapsableContentType = ({
- controllers,
+interface CollapsableContentTypeProps {
+ controllers?: ContentApiPermission['controllers'];
+ label: ContentApiPermission['label'];
+ orderNumber?: number;
+ disabled?: boolean;
+ onExpanded?: (orderNumber: number) => void;
+ indexExpandendCollapsedContent: number | null;
+}
+
+export const CollapsableContentType = ({
+ controllers = [],
label,
- orderNumber,
- disabled,
- onExpanded,
- indexExpandendCollapsedContent,
-}) => {
+ orderNumber = 0,
+ disabled = false,
+ onExpanded = () => null,
+ indexExpandendCollapsedContent = null,
+}: CollapsableContentTypeProps) => {
const {
value: { onChangeSelectAll, onChange, selectedActions, setSelectedAction, selectedAction },
} = useApiTokenPermissions();
- const [expanded, setExpanded] = useState(false);
+ const [expanded, setExpanded] = React.useState(false);
const { formatMessage } = useIntl();
const handleExpandedAccordion = () => {
@@ -46,7 +79,7 @@ const CollapsableContentType = ({
onExpanded(orderNumber);
};
- useEffect(() => {
+ React.useEffect(() => {
if (
indexExpandendCollapsedContent !== null &&
indexExpandendCollapsedContent !== orderNumber &&
@@ -56,7 +89,7 @@ const CollapsableContentType = ({
}
}, [indexExpandendCollapsedContent, orderNumber, expanded]);
- const isActionSelected = (actionId) => actionId === selectedAction;
+ const isActionSelected = (actionId: string) => actionId === selectedAction;
return (
);
};
-
-CollapsableContentType.defaultProps = {
- controllers: [],
- orderNumber: 0,
- disabled: false,
- onExpanded: () => null,
- indexExpandendCollapsedContent: null,
-};
-
-CollapsableContentType.propTypes = {
- controllers: PropTypes.array,
- orderNumber: PropTypes.number,
- label: PropTypes.string.isRequired,
- disabled: PropTypes.bool,
- onExpanded: PropTypes.func,
- indexExpandendCollapsedContent: PropTypes.number,
-};
-
-export default CollapsableContentType;
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/CollapsableContentType/CheckBoxWrapper.js b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/CollapsableContentType/CheckBoxWrapper.js
deleted file mode 100644
index e3165e258d..0000000000
--- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/CollapsableContentType/CheckBoxWrapper.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import { Box } from '@strapi/design-system';
-import styled, { css } from 'styled-components';
-
-const activeCheckboxWrapperStyles = css`
- background: ${(props) => props.theme.colors.primary100};
- svg {
- opacity: 1;
- }
-`;
-
-const CheckboxWrapper = styled(Box)`
- display: flex;
- justify-content: space-between;
- align-items: center;
-
- svg {
- opacity: 0;
- path {
- fill: ${(props) => props.theme.colors.primary600};
- }
- }
-
- /* Show active style both on hover and when the action is selected */
- ${(props) => props.isActive && activeCheckboxWrapperStyles}
- &:hover {
- ${activeCheckboxWrapperStyles}
- }
-`;
-
-export default CheckboxWrapper;
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/ContenTypesSection/index.js b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/ContentTypesSection.tsx
similarity index 52%
rename from packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/ContenTypesSection/index.js
rename to packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/ContentTypesSection.tsx
index c9e4383654..08776d4059 100644
--- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/ContenTypesSection/index.js
+++ b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/ContentTypesSection.tsx
@@ -1,13 +1,21 @@
-import React, { useState } from 'react';
+import * as React from 'react';
import { Box } from '@strapi/design-system';
-import PropTypes from 'prop-types';
-import CollapsableContentType from '../CollapsableContentType';
+import { ContentApiPermission } from '../../../../../../../../shared/contracts/content-api/permissions';
-const ContentTypesSection = ({ section, ...props }) => {
- const [indexExpandedCollpsedContent, setIndexExpandedCollpsedContent] = useState(null);
- const handleExpandedCollpsedContentIndex = (index) => setIndexExpandedCollpsedContent(index);
+import { CollapsableContentType } from './CollabsableContentType';
+
+interface ContentTypesSectionProps {
+ section: ContentApiPermission[] | null;
+}
+
+export const ContentTypesSection = ({ section = null, ...props }: ContentTypesSectionProps) => {
+ const [indexExpandedCollpsedContent, setIndexExpandedCollpsedContent] = React.useState<
+ null | number
+ >(null);
+ const handleExpandedCollpsedContentIndex = (index: number) =>
+ setIndexExpandedCollpsedContent(index);
return (
@@ -20,20 +28,9 @@ const ContentTypesSection = ({ section, ...props }) => {
orderNumber={index}
indexExpandendCollapsedContent={indexExpandedCollpsedContent}
onExpanded={handleExpandedCollpsedContentIndex}
- name={api.apiId}
{...props}
/>
))}
);
};
-
-ContentTypesSection.defaultProps = {
- section: null,
-};
-
-ContentTypesSection.propTypes = {
- section: PropTypes.arrayOf(PropTypes.object),
-};
-
-export default ContentTypesSection;
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/FormApiTokenContainer/index.js b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/FormApiTokenContainer.tsx
similarity index 63%
rename from packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/FormApiTokenContainer/index.js
rename to packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/FormApiTokenContainer.tsx
index f0bfe1ea20..b7f8e8e58d 100644
--- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/FormApiTokenContainer/index.js
+++ b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/FormApiTokenContainer.tsx
@@ -1,27 +1,50 @@
-import React from 'react';
+import * as React from 'react';
import { Box, Flex, Grid, GridItem, Typography } from '@strapi/design-system';
-import PropTypes from 'prop-types';
+import { FormikErrors } from 'formik';
import { useIntl } from 'react-intl';
-import LifeSpanInput from '../../../../../components/Tokens/LifeSpanInput';
-import TokenDescription from '../../../../../components/Tokens/TokenDescription';
-import TokenName from '../../../../../components/Tokens/TokenName';
-import TokenTypeSelect from '../../../../../components/Tokens/TokenTypeSelect';
+// @ts-expect-error not converted yet
+import LifeSpanInput from '../../../../components/Tokens/LifeSpanInput';
+// @ts-expect-error not converted yet
+import TokenDescription from '../../../../components/Tokens/TokenDescription';
+// @ts-expect-error not converted yet
+import TokenName from '../../../../components/Tokens/TokenName';
+// @ts-expect-error not converted yet
+import TokenTypeSelect from '../../../../components/Tokens/TokenTypeSelect';
-const FormApiTokenContainer = ({
- errors,
+type TokenType = 'full-access' | 'read-only' | 'custom';
+
+import type { ApiToken } from '../../../../../../../../shared/contracts/api-token';
+
+interface FormApiTokenContainerProps {
+ errors?: FormikErrors>;
+ onChange: ({ target: { name, value } }: { target: { name: string; value: TokenType } }) => void;
+ canEditInputs: boolean;
+ values: undefined | Partial>;
+ isCreating: boolean;
+ apiToken?: null | Partial;
+ onDispatch: React.Dispatch;
+ setHasChangedPermissions: (hasChanged: boolean) => void;
+}
+
+export const FormApiTokenContainer = ({
+ errors = {},
onChange,
canEditInputs,
isCreating,
values,
- apiToken,
+ apiToken = {},
onDispatch,
setHasChangedPermissions,
-}) => {
+}: FormApiTokenContainerProps) => {
const { formatMessage } = useIntl();
- const handleChangeSelectApiTokenType = ({ target: { value } }) => {
+ const handleChangeSelectApiTokenType = ({
+ target: { value },
+ }: {
+ target: { value: TokenType };
+ }) => {
setHasChangedPermissions(false);
if (value === 'full-access') {
@@ -112,7 +135,7 @@ const FormApiTokenContainer = ({
id: 'Settings.tokens.form.type',
defaultMessage: 'Token type',
}}
- onChange={(value) => {
+ onChange={(value: TokenType) => {
handleChangeSelectApiTokenType({ target: { value } });
onChange({ target: { name: 'type', value } });
}}
@@ -125,40 +148,3 @@ const FormApiTokenContainer = ({
);
};
-
-FormApiTokenContainer.propTypes = {
- errors: PropTypes.shape({
- name: PropTypes.string,
- description: PropTypes.string,
- lifespan: PropTypes.string,
- type: PropTypes.string,
- }),
- onChange: PropTypes.func.isRequired,
- canEditInputs: PropTypes.bool.isRequired,
- values: PropTypes.shape({
- name: PropTypes.string,
- description: PropTypes.string,
- lifespan: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
- type: PropTypes.string,
- }).isRequired,
- isCreating: PropTypes.bool.isRequired,
- apiToken: PropTypes.shape({
- id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
- type: PropTypes.string,
- lifespan: PropTypes.string,
- name: PropTypes.string,
- accessKey: PropTypes.string,
- permissions: PropTypes.array,
- description: PropTypes.string,
- createdAt: PropTypes.string,
- }),
- onDispatch: PropTypes.func.isRequired,
- setHasChangedPermissions: PropTypes.func.isRequired,
-};
-
-FormApiTokenContainer.defaultProps = {
- errors: {},
- apiToken: {},
-};
-
-export default FormApiTokenContainer;
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/LoadingView/index.js b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/LoadingView.tsx
similarity index 78%
rename from packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/LoadingView/index.js
rename to packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/LoadingView.tsx
index 6c01c069b7..286a0389c0 100644
--- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/LoadingView/index.js
+++ b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/LoadingView.tsx
@@ -1,5 +1,3 @@
-import React from 'react';
-
import { Button, ContentLayout, HeaderLayout, Main } from '@strapi/design-system';
import {
LoadingIndicatorPage,
@@ -7,10 +5,13 @@ import {
useFocusWhenNavigate,
} from '@strapi/helper-plugin';
import { Check } from '@strapi/icons';
-import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
-const LoadingView = ({ apiTokenName }) => {
+interface LoadingViewProps {
+ apiTokenName?: string | null;
+}
+
+export const LoadingView = ({ apiTokenName = null }: LoadingViewProps) => {
const { formatMessage } = useIntl();
useFocusWhenNavigate();
@@ -37,13 +38,3 @@ const LoadingView = ({ apiTokenName }) => {
);
};
-
-LoadingView.defaultProps = {
- apiTokenName: null,
-};
-
-LoadingView.propTypes = {
- apiTokenName: PropTypes.string,
-};
-
-export default LoadingView;
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/Permissions/index.js b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/Permissions.tsx
similarity index 78%
rename from packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/Permissions/index.js
rename to packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/Permissions.tsx
index b791883d08..a703eba111 100644
--- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/Permissions/index.js
+++ b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/Permissions.tsx
@@ -1,13 +1,12 @@
-import React, { memo } from 'react';
-
import { Flex, Grid, GridItem, Typography } from '@strapi/design-system';
import { useIntl } from 'react-intl';
-import { useApiTokenPermissions } from '../../../../../../../contexts/apiTokenPermissions';
-import ActionBoundRoutes from '../ActionBoundRoutes';
-import ContentTypesSection from '../ContenTypesSection';
+import { useApiTokenPermissions } from '../../../../../../contexts/apiTokenPermissions';
-const Permissions = ({ ...props }) => {
+import { ActionBoundRoutes } from './ActionBoundRoutes';
+import { ContentTypesSection } from './ContentTypesSection';
+
+export const Permissions = ({ ...props }) => {
const {
value: { data },
} = useApiTokenPermissions();
@@ -36,5 +35,3 @@ const Permissions = ({ ...props }) => {
);
};
-
-export default memo(Permissions);
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/Regenerate/index.js b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/Regenerate.tsx
similarity index 76%
rename from packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/Regenerate/index.js
rename to packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/Regenerate.tsx
index 6de90778fa..9f25bba279 100644
--- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/Regenerate/index.js
+++ b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/components/Regenerate.tsx
@@ -1,16 +1,20 @@
-import React, { useState } from 'react';
+import * as React from 'react';
import { Button } from '@strapi/design-system';
import { ConfirmDialog } from '@strapi/helper-plugin';
import { Refresh } from '@strapi/icons';
-import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
-import { useRegenerate } from '../../../../../hooks/useRegenerate';
+import { useRegenerate } from '../../../../hooks/useRegenerate';
-export const Regenerate = ({ onRegenerate, idToRegenerate }) => {
+interface RegenerateProps {
+ onRegenerate?: () => void;
+ idToRegenerate: string | number;
+}
+
+export const Regenerate = ({ onRegenerate = () => {}, idToRegenerate }: RegenerateProps) => {
const { formatMessage } = useIntl();
- const [showConfirmDialog, setShowConfirmDialog] = useState(false);
+ const [showConfirmDialog, setShowConfirmDialog] = React.useState(false);
const { regenerateData, isLoadingConfirmation } = useRegenerate(
'/admin/api-tokens/',
idToRegenerate,
@@ -63,12 +67,3 @@ export const Regenerate = ({ onRegenerate, idToRegenerate }) => {
>
);
};
-
-Regenerate.defaultProps = { onRegenerate() {} };
-
-Regenerate.propTypes = {
- onRegenerate: PropTypes.func,
- idToRegenerate: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
-};
-
-export default Regenerate;
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/utils/schema.js b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/constants.ts
similarity index 63%
rename from packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/utils/schema.js
rename to packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/constants.ts
index 5e1621102f..a0747fe31b 100644
--- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/utils/schema.js
+++ b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/constants.ts
@@ -1,14 +1,12 @@
import { translatedErrors } from '@strapi/helper-plugin';
import * as yup from 'yup';
-const schema = yup.object().shape({
- name: yup.string(translatedErrors.string).max(100).required(translatedErrors.required),
+export const schema = yup.object().shape({
+ name: yup.string().max(100).required(translatedErrors.required),
type: yup
- .string(translatedErrors.string)
+ .string()
.oneOf(['read-only', 'full-access', 'custom'])
.required(translatedErrors.required),
description: yup.string().nullable(),
lifespan: yup.number().integer().min(0).nullable().defined(translatedErrors.required),
});
-
-export default schema;
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/init.js b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/init.js
deleted file mode 100644
index a9ba8b9377..0000000000
--- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/init.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import { transformPermissionsData } from './utils';
-
-const init = (state, permissions = []) => {
- return {
- ...state,
- selectedAction: null,
- routes: [],
- selectedActions: [],
- data: transformPermissionsData(permissions),
- };
-};
-
-export default init;
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/reducer.js b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/reducer.ts
similarity index 53%
rename from packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/reducer.js
rename to packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/reducer.ts
index 160a505186..655eee6235 100644
--- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/reducer.js
+++ b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/reducer.ts
@@ -2,14 +2,75 @@
import produce from 'immer';
import pull from 'lodash/pull';
-import { transformPermissionsData } from './utils';
+import { ContentApiPermission } from '../../../../../../../shared/contracts/content-api/permissions';
+import { ApiTokenPermissionsContextValue } from '../../../../../contexts/apiTokenPermissions';
-export const initialState = {
- data: {},
+import { transformPermissionsData } from './utils/transformPermissionsData';
+
+type InitialState = Pick<
+ ApiTokenPermissionsContextValue['value'],
+ 'data' | 'routes' | 'selectedAction' | 'selectedActions'
+>;
+
+interface ActionOnChange {
+ type: 'ON_CHANGE';
+ value: string;
+}
+
+interface ActionSelectAllInPermission {
+ type: 'SELECT_ALL_IN_PERMISSION';
+ value: { action: string; actionId: string }[];
+}
+
+interface ActionSelectAllActions {
+ type: 'SELECT_ALL_ACTIONS';
+}
+
+interface ActionOnChangeReadOnly {
+ type: 'ON_CHANGE_READ_ONLY';
+}
+
+interface ActionUpdatePermissionsLayout {
+ type: 'UPDATE_PERMISSIONS_LAYOUT';
+ value: ContentApiPermission;
+}
+
+interface ActionUpdateRoutes {
+ type: 'UPDATE_ROUTES';
+ value: ApiTokenPermissionsContextValue['value']['routes'] | undefined;
+}
+
+interface ActionUpdatePermissions {
+ type: 'UPDATE_PERMISSIONS';
+ value: any[];
+}
+
+interface ActionSetSelectedAction {
+ type: 'SET_SELECTED_ACTION';
+ value: string;
+}
+
+type Action =
+ | ActionOnChange
+ | ActionSelectAllInPermission
+ | ActionSelectAllActions
+ | ActionOnChangeReadOnly
+ | ActionUpdatePermissionsLayout
+ | ActionUpdateRoutes
+ | ActionUpdatePermissions
+ | ActionSetSelectedAction;
+
+export const initialState: InitialState = {
+ data: {
+ allActionsIds: [],
+ permissions: [],
+ },
+ routes: {},
+ selectedAction: '',
selectedActions: [],
};
-const reducer = (state, action) =>
+export const reducer = (state: typeof initialState, action: Action) =>
produce(state, (draftState) => {
switch (action.type) {
case 'ON_CHANGE': {
@@ -69,5 +130,3 @@ const reducer = (state, action) =>
return draftState;
}
});
-
-export default reducer;
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/tests/index.test.js b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/tests/index.test.js
index 18ac130492..612b6f7af7 100644
--- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/tests/index.test.js
+++ b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/tests/index.test.js
@@ -11,8 +11,22 @@ import { Route, MemoryRouter } from 'react-router-dom';
import { Theme } from '../../../../../../components/Theme';
import { ThemeToggleProvider } from '../../../../../../components/ThemeToggleProvider';
-import EditView from '../index';
-import { data } from '../utils/tests/dataMock';
+import { EditView } from '../EditViewPage';
+
+const data = {
+ data: {
+ 'api::address': {
+ controllers: {
+ address: ['find', 'findOne'],
+ },
+ },
+ 'api::category': {
+ controllers: {
+ category: ['find', 'findOne', 'create', 'update', 'delete', 'createLocalization'],
+ },
+ },
+ },
+};
jest.mock('@strapi/helper-plugin', () => ({
...jest.requireActual('@strapi/helper-plugin'),
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/tests/reducer.test.js b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/tests/reducer.test.js
index bf0690f65b..adefe30860 100644
--- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/tests/reducer.test.js
+++ b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/tests/reducer.test.js
@@ -1,9 +1,24 @@
-import init from '../init';
-import reducer from '../reducer';
-import { data } from '../utils/tests/dataMock';
+import { reducer } from '../reducer';
+import { transformPermissionsData } from '../utils/transformPermissionsData';
describe('ADMIN | Pages | API TOKENS | EditView | reducer', () => {
- const initialState = init({}, data.data);
+ const initialState = {
+ selectedAction: null,
+ routes: [],
+ selectedActions: [],
+ data: transformPermissionsData({
+ 'api::address': {
+ controllers: {
+ address: ['find', 'findOne'],
+ },
+ },
+ 'api::category': {
+ controllers: {
+ category: ['find', 'findOne', 'create', 'update', 'delete', 'createLocalization'],
+ },
+ },
+ }),
+ };
it('should return the initialState when the type is undefined', () => {
const action = { type: undefined };
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/utils/getDateOfExpiration.js b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/utils/getDateOfExpiration.ts
similarity index 66%
rename from packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/utils/getDateOfExpiration.js
rename to packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/utils/getDateOfExpiration.ts
index b5d3439ef9..002acb5291 100644
--- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/utils/getDateOfExpiration.js
+++ b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/utils/getDateOfExpiration.ts
@@ -1,16 +1,19 @@
import { addDays, format } from 'date-fns';
import * as locales from 'date-fns/locale';
-const getDateOfExpiration = (createdAt, duration, language = 'en') => {
+export const getDateOfExpiration = (
+ createdAt: string,
+ duration: number | null,
+ language: string = 'en'
+) => {
if (duration && typeof duration === 'number') {
const durationInDays = duration / 24 / 60 / 60 / 1000;
return format(addDays(new Date(createdAt), durationInDays), 'PPP', {
+ // @ts-expect-error I don't know how to fix this
locale: locales[language],
});
}
return 'Unlimited';
};
-
-export default getDateOfExpiration;
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/utils/index.js b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/utils/index.js
deleted file mode 100644
index b88422c87e..0000000000
--- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/utils/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import getDateOfExpiration from './getDateOfExpiration';
-import schema from './schema';
-import transformPermissionsData from './transformPermissionsData';
-
-export { getDateOfExpiration, schema, transformPermissionsData };
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/utils/tests/dataMock.js b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/utils/tests/dataMock.js
deleted file mode 100644
index 8f23118000..0000000000
--- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/utils/tests/dataMock.js
+++ /dev/null
@@ -1,14 +0,0 @@
-export const data = {
- data: {
- 'api::address': {
- controllers: {
- address: ['find', 'findOne'],
- },
- },
- 'api::category': {
- controllers: {
- category: ['find', 'findOne', 'create', 'update', 'delete', 'createLocalization'],
- },
- },
- },
-};
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/utils/tests/getDateOfExpiration.test.js b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/utils/tests/getDateOfExpiration.test.js
index 5cbc3d8b28..bfc12ac5e2 100644
--- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/utils/tests/getDateOfExpiration.test.js
+++ b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/utils/tests/getDateOfExpiration.test.js
@@ -1,4 +1,4 @@
-import getDateOfExpiration from '../getDateOfExpiration';
+import { getDateOfExpiration } from '../getDateOfExpiration';
const createdAt = '2022-07-05T12:16:56.821Z';
const duration = 604800000;
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/utils/tests/transformPermissionsData.test.js b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/utils/tests/transformPermissionsData.test.js
index af7f99984d..f5c0534097 100644
--- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/utils/tests/transformPermissionsData.test.js
+++ b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/utils/tests/transformPermissionsData.test.js
@@ -1,6 +1,19 @@
-import transformPermissionsData from '../transformPermissionsData';
+import { transformPermissionsData } from '../transformPermissionsData';
-import { data } from './dataMock';
+const data = {
+ data: {
+ 'api::address': {
+ controllers: {
+ address: ['find', 'findOne'],
+ },
+ },
+ 'api::category': {
+ controllers: {
+ category: ['find', 'findOne', 'create', 'update', 'delete', 'createLocalization'],
+ },
+ },
+ },
+};
describe('ADMIN | Container | SettingsPage | ApiTokens | EditView | utils | transformPermissionsData', () => {
it('should return transformed data correctly', () => {
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/utils/transformPermissionsData.js b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/utils/transformPermissionsData.js
deleted file mode 100644
index c6a8e5b8cb..0000000000
--- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/utils/transformPermissionsData.js
+++ /dev/null
@@ -1,34 +0,0 @@
-const transformPermissionsData = (data) => {
- const layout = {
- allActionsIds: [],
- permissions: [],
- };
-
- layout.permissions = Object.keys(data).map((apiId) => ({
- apiId,
- label: apiId.split('::')[1],
- controllers: Object.keys(data[apiId].controllers)
- .map((controller) => ({
- controller,
- actions: data[apiId].controllers[controller]
- .map((action) => {
- const actionId = `${apiId}.${controller}.${action}`;
-
- if (apiId.includes('api::')) {
- layout.allActionsIds.push(actionId);
- }
-
- return {
- action,
- actionId,
- };
- })
- .flat(),
- }))
- .flat(),
- }));
-
- return layout;
-};
-
-export default transformPermissionsData;
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/utils/transformPermissionsData.ts b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/utils/transformPermissionsData.ts
new file mode 100644
index 0000000000..7d4fdb1b98
--- /dev/null
+++ b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/EditView/utils/transformPermissionsData.ts
@@ -0,0 +1,46 @@
+import { ContentApiPermission } from '../../../../../../../../shared/contracts/content-api/permissions';
+
+interface Layout {
+ allActionsIds: string[];
+ permissions: {
+ apiId: string;
+ label: string;
+ controllers: { controller: string; actions: { action: string; actionId: string }[] }[];
+ }[];
+}
+
+export const transformPermissionsData = (data: ContentApiPermission) => {
+ const layout: Layout = {
+ allActionsIds: [],
+ permissions: [],
+ };
+
+ layout.permissions = Object.entries(data).map(([apiId, permission]) => ({
+ apiId,
+ label: apiId.split('::')[1],
+ controllers: Object.keys(permission.controllers)
+ .map((controller) => ({
+ controller,
+ actions:
+ controller in permission.controllers
+ ? permission.controllers[controller]
+ .map((action: ContentApiPermission['controllers']) => {
+ const actionId = `${apiId}.${controller}.${action}`;
+
+ if (apiId.includes('api::')) {
+ layout.allActionsIds.push(actionId);
+ }
+
+ return {
+ action,
+ actionId,
+ };
+ })
+ .flat()
+ : [],
+ }))
+ .flat(),
+ }));
+
+ return layout;
+};
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/ListView/index.js b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/ListView.tsx
similarity index 69%
rename from packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/ListView/index.js
rename to packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/ListView.tsx
index 4f17cb79cd..90ca473079 100644
--- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/ListView/index.js
+++ b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/ListView.tsx
@@ -1,8 +1,8 @@
import * as React from 'react';
-import { ContentLayout, HeaderLayout, Main } from '@strapi/design-system';
+import { ContentLayout, HeaderLayout, LinkButton, Main } from '@strapi/design-system';
import {
- LinkButton,
+ CheckPagePermissions,
NoContent,
NoPermissions,
SettingsPageTitle,
@@ -15,19 +15,68 @@ import {
useTracking,
} from '@strapi/helper-plugin';
import { Plus } from '@strapi/icons';
+import { AxiosError } from 'axios';
import qs from 'qs';
import { useIntl } from 'react-intl';
import { useMutation, useQuery, useQueryClient } from 'react-query';
import { useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
-import { selectAdminPermissions } from '../../../../../selectors';
-import { API_TOKEN_TYPE } from '../../../components/Tokens/constants';
-import Table from '../../../components/Tokens/Table';
+import { selectAdminPermissions } from '../../../../selectors';
+import { API_TOKEN_TYPE } from '../../components/Tokens/constants';
+// @ts-expect-error not converted yet
+import Table from '../../components/Tokens/Table';
-import tableHeaders from './utils/tableHeaders';
+import type { List } from '../../../../../../shared/contracts/api-token';
-const ApiTokenListView = () => {
+const TABLE_HEADERS = [
+ {
+ name: 'name',
+ key: 'name',
+ metadatas: {
+ label: {
+ id: 'Settings.apiTokens.ListView.headers.name',
+ defaultMessage: 'Name',
+ },
+ sortable: true,
+ },
+ },
+ {
+ name: 'description',
+ key: 'description',
+ metadatas: {
+ label: {
+ id: 'Settings.apiTokens.ListView.headers.description',
+ defaultMessage: 'Description',
+ },
+ sortable: false,
+ },
+ },
+ {
+ name: 'createdAt',
+ key: 'createdAt',
+ metadatas: {
+ label: {
+ id: 'Settings.apiTokens.ListView.headers.createdAt',
+ defaultMessage: 'Created at',
+ },
+ sortable: false,
+ },
+ },
+ {
+ name: 'lastUsedAt',
+ key: 'lastUsedAt',
+ metadatas: {
+ label: {
+ id: 'Settings.apiTokens.ListView.headers.lastUsedAt',
+ defaultMessage: 'Last used',
+ },
+ sortable: false,
+ },
+ },
+];
+
+export const ListView = () => {
useFocusWhenNavigate();
const queryClient = useQueryClient();
const { formatMessage } = useIntl();
@@ -35,7 +84,7 @@ const ApiTokenListView = () => {
const permissions = useSelector(selectAdminPermissions);
const {
allowedActions: { canCreate, canDelete, canUpdate, canRead },
- } = useRBAC(permissions.settings['api-tokens']);
+ } = useRBAC(permissions.settings?.['api-tokens']);
const { push } = useHistory();
const { trackUsage } = useTracking();
const { startSection } = useGuidedTour();
@@ -53,7 +102,7 @@ const ApiTokenListView = () => {
push({ search: qs.stringify({ sort: 'name:ASC' }, { encode: false }) });
}, [push]);
- const headers = tableHeaders.map((header) => ({
+ const headers = TABLE_HEADERS.map((header) => ({
...header,
metadatas: {
...header.metadatas,
@@ -70,7 +119,7 @@ const ApiTokenListView = () => {
const {
data: { data },
- } = await get(`/admin/api-tokens`);
+ } = await get(`/admin/api-tokens`);
trackUsage('didAccessTokenList', { number: data.length, tokenType: API_TOKEN_TYPE });
@@ -80,10 +129,12 @@ const ApiTokenListView = () => {
cacheTime: 0,
enabled: canRead,
onError(error) {
- toggleNotification({
- type: 'warning',
- message: formatAPIError(error),
- });
+ if (error instanceof AxiosError) {
+ toggleNotification({
+ type: 'warning',
+ message: formatAPIError(error),
+ });
+ }
},
}
);
@@ -100,7 +151,9 @@ const ApiTokenListView = () => {
trackUsage('didDeleteToken');
},
onError(error) {
- toggleNotification({ type: 'warning', message: formatAPIError(error) });
+ if (error instanceof AxiosError) {
+ toggleNotification({ type: 'warning', message: formatAPIError(error) });
+ }
},
}
);
@@ -150,6 +203,7 @@ const ApiTokenListView = () => {
contentType="api-tokens"
rows={apiTokens}
isLoading={isLoading}
+ // @ts-expect-error not converted yet
onConfirmDelete={(id) => deleteMutation.mutateAsync(id)}
tokens={apiTokens}
tokenType={API_TOKEN_TYPE}
@@ -184,4 +238,12 @@ const ApiTokenListView = () => {
);
};
-export default ApiTokenListView;
+export const ProtectedListView = () => {
+ const permissions = useSelector(selectAdminPermissions);
+
+ return (
+
+
+
+ );
+};
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/ListView/utils/tableHeaders.js b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/ListView/utils/tableHeaders.js
deleted file mode 100644
index a1fc8b170c..0000000000
--- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/ListView/utils/tableHeaders.js
+++ /dev/null
@@ -1,48 +0,0 @@
-const tableHeaders = [
- {
- name: 'name',
- key: 'name',
- metadatas: {
- label: {
- id: 'Settings.apiTokens.ListView.headers.name',
- defaultMessage: 'Name',
- },
- sortable: true,
- },
- },
- {
- name: 'description',
- key: 'description',
- metadatas: {
- label: {
- id: 'Settings.apiTokens.ListView.headers.description',
- defaultMessage: 'Description',
- },
- sortable: false,
- },
- },
- {
- name: 'createdAt',
- key: 'createdAt',
- metadatas: {
- label: {
- id: 'Settings.apiTokens.ListView.headers.createdAt',
- defaultMessage: 'Created at',
- },
- sortable: false,
- },
- },
- {
- name: 'lastUsedAt',
- key: 'lastUsedAt',
- metadatas: {
- label: {
- id: 'Settings.apiTokens.ListView.headers.lastUsedAt',
- defaultMessage: 'Last used',
- },
- sortable: false,
- },
- },
-];
-
-export default tableHeaders;
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/ProtectedCreateView/index.js b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/ProtectedCreateView/index.js
deleted file mode 100644
index ef77644990..0000000000
--- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/ProtectedCreateView/index.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import React from 'react';
-
-import { CheckPagePermissions } from '@strapi/helper-plugin';
-import { useSelector } from 'react-redux';
-
-import { selectAdminPermissions } from '../../../../../selectors';
-import EditView from '../EditView';
-
-const ProtectedApiTokenCreateView = () => {
- const permissions = useSelector(selectAdminPermissions);
-
- return (
-
-
-
- );
-};
-
-export default ProtectedApiTokenCreateView;
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/ProtectedEditView/index.js b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/ProtectedEditView/index.js
deleted file mode 100644
index f1a643c941..0000000000
--- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/ProtectedEditView/index.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import React from 'react';
-
-import { CheckPagePermissions } from '@strapi/helper-plugin';
-import { useSelector } from 'react-redux';
-
-import { selectAdminPermissions } from '../../../../../selectors';
-import EditView from '../EditView';
-
-const ProtectedApiTokenCreateView = () => {
- const permissions = useSelector(selectAdminPermissions);
-
- return (
-
-
-
- );
-};
-
-export default ProtectedApiTokenCreateView;
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/ProtectedListView/index.js b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/ProtectedListView/index.js
deleted file mode 100644
index 5d61fc72bb..0000000000
--- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/ProtectedListView/index.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import React from 'react';
-
-import { CheckPagePermissions } from '@strapi/helper-plugin';
-import { useSelector } from 'react-redux';
-
-import { selectAdminPermissions } from '../../../../../selectors';
-import ListView from '../ListView';
-
-const ProtectedApiTokenListView = () => {
- const permissions = useSelector(selectAdminPermissions);
-
- return (
-
-
-
- );
-};
-
-export default ProtectedApiTokenListView;
diff --git a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/ListView/tests/index.test.js b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/tests/ListView.test.tsx
similarity index 85%
rename from packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/ListView/tests/index.test.js
rename to packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/tests/ListView.test.tsx
index 0fde16603c..3ea01027ec 100644
--- a/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/ListView/tests/index.test.js
+++ b/packages/core/admin/admin/src/pages/Settings/pages/ApiTokens/tests/ListView.test.tsx
@@ -10,9 +10,9 @@ import { QueryClient, QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux';
import { Route, MemoryRouter } from 'react-router-dom';
-import { Theme } from '../../../../../../components/Theme';
-import { ThemeToggleProvider } from '../../../../../../components/ThemeToggleProvider';
-import ListView from '../index';
+import { Theme } from '../../../../../components/Theme';
+import { ThemeToggleProvider } from '../../../../../components/ThemeToggleProvider';
+import { ListView } from '../ListView';
jest.mock('@strapi/helper-plugin', () => ({
...jest.requireActual('@strapi/helper-plugin'),
@@ -46,8 +46,6 @@ jest.mock('@strapi/helper-plugin', () => ({
}),
}));
-jest.spyOn(Date, 'now').mockImplementation(() => new Date('2015-10-01T08:00:00.000Z'));
-
// TO BE REMOVED: we have added this mock to prevent errors in the snapshots caused by the Unicode space character
// before AM/PM in the dates, after the introduction of node 18.13
jest.mock('react-intl', () => {
@@ -65,8 +63,8 @@ jest.mock('react-intl', () => {
};
});
-const setup = ({ path, ...props } = {}) =>
- render(() => , {
+const setup = ({ path = '/settings/api-tokens', ...props } = {}) =>
+ render(, {
wrapper({ children }) {
const client = new QueryClient({
defaultOptions: {
@@ -90,7 +88,7 @@ const setup = ({ path, ...props } = {}) =>
-
+
{children}
@@ -109,6 +107,7 @@ describe('ADMIN | Pages | API TOKENS | ListPage', () => {
});
it('should show a list of api tokens', async () => {
+ // @ts-expect-error this is fine
useRBAC.mockReturnValue({
allowedActions: {
canCreate: true,
@@ -121,11 +120,14 @@ describe('ADMIN | Pages | API TOKENS | ListPage', () => {
const { getByText } = setup({ path: '/settings/api-tokens' });
+ // eslint-disable-next-line testing-library/prefer-find-by
await waitFor(() => expect(getByText('My super token')).toBeInTheDocument());
+ // eslint-disable-next-line testing-library/prefer-find-by
await waitFor(() => expect(getByText('This describe my super token')).toBeInTheDocument());
});
it('should not show the create button when the user does not have the rights to create', async () => {
+ // @ts-expect-error this is fine
useRBAC.mockReturnValue({
allowedActions: {
canCreate: false,
@@ -142,6 +144,7 @@ describe('ADMIN | Pages | API TOKENS | ListPage', () => {
});
it('should show the delete button when the user have the rights to delete', async () => {
+ // @ts-expect-error this is fine
useRBAC.mockReturnValue({
allowedActions: {
canCreate: false,
@@ -155,11 +158,13 @@ describe('ADMIN | Pages | API TOKENS | ListPage', () => {
const { container } = setup();
await waitFor(() =>
+ // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
expect(container.querySelector('button[name="delete"]')).toBeInTheDocument()
);
});
it('should show the read button when the user have the rights to read and not to update', async () => {
+ // @ts-expect-error this is fine
useRBAC.mockReturnValue({
allowedActions: {
canCreate: false,
@@ -172,6 +177,7 @@ describe('ADMIN | Pages | API TOKENS | ListPage', () => {
const { container } = setup();
+ // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
await waitFor(() => expect(container.querySelector('a[title*="Read"]')).toBeInTheDocument());
});
});
diff --git a/packages/core/admin/admin/src/types/permissions.ts b/packages/core/admin/admin/src/types/permissions.ts
index 46c8b7ec2f..60acb8d453 100644
--- a/packages/core/admin/admin/src/types/permissions.ts
+++ b/packages/core/admin/admin/src/types/permissions.ts
@@ -1,6 +1,12 @@
import type { Permission } from '@strapi/helper-plugin';
-type SettingsPermissions = 'project-settings' | 'users' | 'webhooks' | 'sso' | 'roles';
+type SettingsPermissions =
+ | 'api-tokens'
+ | 'project-settings'
+ | 'roles'
+ | 'sso'
+ | 'users'
+ | 'webhooks';
interface CRUDPermissions {
main: Permission[];
diff --git a/packages/core/admin/server/src/services/api-token.ts b/packages/core/admin/server/src/services/api-token.ts
index 78534c697c..9f85b46ae9 100644
--- a/packages/core/admin/server/src/services/api-token.ts
+++ b/packages/core/admin/server/src/services/api-token.ts
@@ -1,7 +1,7 @@
import crypto from 'crypto';
import { omit, difference, isNil, isEmpty, map, isArray, uniq } from 'lodash/fp';
import { errors } from '@strapi/utils';
-import type { Update } from '../../../shared/contracts/api-token';
+import type { Update, ApiToken, ApiTokenBody } from '../../../shared/contracts/api-token';
import constants from './constants';
const { ValidationError, NotFoundError } = errors;
@@ -9,23 +9,13 @@ const { ValidationError, NotFoundError } = errors;
type ApiTokenPermission = {
id: number | `${number}`;
action: string;
- token: ApiToken | number;
+ token: DBApiToken | number;
};
-export type ApiToken = {
- id?: number | `${number}`;
- name: string;
- description: string;
- accessKey: string;
- lastUsedAt: number;
- lifespan: number | null;
- expiresAt: number;
- type: 'read-only' | 'full-access' | 'custom';
+type DBApiToken = ApiToken & {
permissions: (number | ApiTokenPermission)[];
};
-export type CreateActionPayload = Omit;
-
const SELECT_FIELDS = [
'id',
'name',
@@ -45,23 +35,24 @@ const POPULATE_FIELDS = ['permissions'];
/**
* Assert that a token's permissions attribute is valid for its type
*/
-const assertCustomTokenPermissionsValidity = (attributes: CreateActionPayload) => {
+const assertCustomTokenPermissionsValidity = (
+ type: ApiTokenBody['type'],
+ permissions: ApiTokenBody['permissions']
+) => {
// Ensure non-custom tokens doesn't have permissions
- if (attributes.type !== constants.API_TOKEN_TYPE.CUSTOM && !isEmpty(attributes.permissions)) {
+ if (type !== constants.API_TOKEN_TYPE.CUSTOM && !isEmpty(permissions)) {
throw new ValidationError('Non-custom tokens should not reference permissions');
}
// Custom type tokens should always have permissions attached to them
- if (attributes.type === constants.API_TOKEN_TYPE.CUSTOM && !isArray(attributes.permissions)) {
+ if (type === constants.API_TOKEN_TYPE.CUSTOM && !isArray(permissions)) {
throw new ValidationError('Missing permissions attribute for custom token');
}
// Permissions provided for a custom type token should be valid/registered permissions UID
- if (attributes.type === constants.API_TOKEN_TYPE.CUSTOM) {
+ if (type === constants.API_TOKEN_TYPE.CUSTOM) {
const validPermissions = strapi.contentAPI.permissions.providers.action.keys();
- // TODO difference requires that the 2 arguments are of the same type
- // @ts-expect-error - Handle type miss match between string[] and number[]
- const invalidPermissions = difference(attributes.permissions, validPermissions) as string[];
+ const invalidPermissions = difference(permissions, validPermissions) as string[];
if (!isEmpty(invalidPermissions)) {
throw new ValidationError(`Unknown permissions provided: ${invalidPermissions.join(', ')}`);
@@ -72,12 +63,12 @@ const assertCustomTokenPermissionsValidity = (attributes: CreateActionPayload) =
/**
* Assert that a token's lifespan is valid
*/
-const assertValidLifespan = ({ lifespan }: CreateActionPayload) => {
+const assertValidLifespan = (lifespan: ApiTokenBody['lifespan']) => {
if (isNil(lifespan)) {
return;
}
- if (!Object.values(constants.API_TOKEN_LIFESPANS).includes(lifespan)) {
+ if (!Object.values(constants.API_TOKEN_LIFESPANS).includes(lifespan as number)) {
throw new ValidationError(
`lifespan must be one of the following values:
${Object.values(constants.API_TOKEN_LIFESPANS).join(', ')}`
@@ -88,7 +79,7 @@ const assertValidLifespan = ({ lifespan }: CreateActionPayload) => {
/**
* Flatten a token's database permissions objects to an array of strings
*/
-const flattenTokenPermissions = (token: ApiToken): ApiToken => {
+const flattenTokenPermissions = (token: DBApiToken): ApiToken => {
if (!token) {
return token;
}
@@ -110,9 +101,7 @@ type WhereParams = {
/**
* Get a token
*/
-const getBy = async (
- whereParams: WhereParams = {}
-): Promise | null> => {
+const getBy = async (whereParams: WhereParams = {}): Promise => {
if (Object.keys(whereParams).length === 0) {
return null;
}
@@ -147,7 +136,7 @@ const hash = (accessKey: string) => {
.digest('hex');
};
-const getExpirationFields = (lifespan: number | null) => {
+const getExpirationFields = (lifespan: ApiTokenBody['lifespan']) => {
// it must be nil or a finite number >= 0
const isValidNumber = Number.isFinite(lifespan) && (lifespan as number) > 0;
if (!isValidNumber && !isNil(lifespan)) {
@@ -156,18 +145,18 @@ const getExpirationFields = (lifespan: number | null) => {
return {
lifespan: lifespan || null,
- expiresAt: lifespan ? Date.now() + lifespan : null,
+ expiresAt: lifespan ? Date.now() + (lifespan as number) : null,
};
};
/**
* Create a token and its permissions
*/
-const create = async (attributes: CreateActionPayload): Promise => {
+const create = async (attributes: ApiTokenBody): Promise => {
const accessKey = crypto.randomBytes(128).toString('hex');
- assertCustomTokenPermissionsValidity(attributes);
- assertValidLifespan(attributes);
+ assertCustomTokenPermissionsValidity(attributes.type, attributes.permissions);
+ assertValidLifespan(attributes.lifespan);
// Create the token
const apiToken: ApiToken = await strapi.query('admin::api-token').create({
@@ -252,8 +241,8 @@ For security reasons, prefer storing the secret in an environment variable and r
/**
* Return a list of all tokens and their permissions
*/
-const list = async (): Promise>> => {
- const tokens: Array = await strapi.query('admin::api-token').findMany({
+const list = async (): Promise> => {
+ const tokens: Array = await strapi.query('admin::api-token').findMany({
select: SELECT_FIELDS,
populate: POPULATE_FIELDS,
orderBy: { name: 'ASC' },
@@ -269,7 +258,7 @@ const list = async (): Promise>> => {
/**
* Revoke (delete) a token
*/
-const revoke = async (id: string | number): Promise> => {
+const revoke = async (id: string | number): Promise => {
return strapi
.query('admin::api-token')
.delete({ select: SELECT_FIELDS, populate: POPULATE_FIELDS, where: { id } });
@@ -295,9 +284,11 @@ const getByName = async (name: string) => {
const update = async (
id: string | number,
attributes: Update.Request['body']
-): Promise> => {
+): Promise => {
// retrieve token without permissions
- const originalToken: ApiToken = await strapi.query('admin::api-token').findOne({ where: { id } });
+ const originalToken: DBApiToken = await strapi
+ .query('admin::api-token')
+ .findOne({ where: { id } });
if (!originalToken) {
throw new NotFoundError('Token not found');
@@ -310,14 +301,13 @@ const update = async (
// if we're updating the permissions on any token type, or changing from non-custom to custom, ensure they're still valid
// if neither type nor permissions are changing, we don't need to validate again or else we can't allow partial update
if (attributes.permissions || changingTypeToCustom) {
- assertCustomTokenPermissionsValidity({
- ...originalToken,
- ...attributes,
- type: attributes.type || originalToken.type,
- });
+ assertCustomTokenPermissionsValidity(
+ attributes.type || originalToken.type,
+ attributes.permissions || originalToken.permissions
+ );
}
- assertValidLifespan(attributes);
+ assertValidLifespan(attributes.lifespan);
const updatedToken: ApiToken = await strapi.query('admin::api-token').update({
select: SELECT_FIELDS,
diff --git a/packages/core/admin/shared/contracts/api-token.ts b/packages/core/admin/shared/contracts/api-token.ts
index f583ae14c7..e325f17340 100644
--- a/packages/core/admin/shared/contracts/api-token.ts
+++ b/packages/core/admin/shared/contracts/api-token.ts
@@ -1,9 +1,25 @@
import { errors } from '@strapi/utils';
+import { Entity } from '@strapi/types';
-import type { ApiToken } from '../../server/src/services/api-token';
+export type ApiToken = {
+ accessKey: string;
+ createdAt: string;
+ description: string;
+ expiresAt: string;
+ id: Entity.ID;
+ lastUsedAt: string | null;
+ lifespan: string | number;
+ name: string;
+ permissions: string[];
+ type: 'custom' | 'full-access' | 'read-only';
+ updatedAt: string;
+};
-type ApiTokenBody = Pick;
-type ApiTokenResponse = Omit;
+export interface ApiTokenBody extends Pick {
+ lifespan?: ApiToken['lifespan'] | null;
+ permissions?: ApiToken['permissions'] | null;
+ type: ApiToken['type'] | undefined;
+}
/**
* POST /api-tokens - Create an api token
@@ -30,7 +46,7 @@ export declare namespace List {
}
export interface Response {
- data: ApiTokenResponse[];
+ data: ApiToken[];
error?: errors.ApplicationError;
}
}
@@ -49,7 +65,7 @@ export declare namespace Revoke {
}
export interface Response {
- data: ApiTokenResponse;
+ data: ApiToken;
error?: errors.ApplicationError;
}
}
@@ -68,7 +84,7 @@ export declare namespace Get {
}
export interface Response {
- data: ApiTokenResponse;
+ data: ApiToken;
error?: errors.ApplicationError;
}
}
@@ -87,7 +103,7 @@ export declare namespace Update {
}
export interface Response {
- data: ApiTokenResponse;
+ data: ApiToken;
error?: errors.ApplicationError | errors.YupValidationError;
}
}
diff --git a/packages/core/admin/shared/contracts/content-api/permissions.ts b/packages/core/admin/shared/contracts/content-api/permissions.ts
new file mode 100644
index 0000000000..7ea9b9002f
--- /dev/null
+++ b/packages/core/admin/shared/contracts/content-api/permissions.ts
@@ -0,0 +1,19 @@
+export interface ContentApiPermission {
+ apiId?: string;
+ label?: string;
+ controllers?: { controller: string; actions: { actionId: string; action: string }[] }[];
+}
+
+/**
+ * GET /content-api/permissions - List content API permissions
+ */
+export declare namespace List {
+ export interface Request {
+ body: {};
+ query: {};
+ }
+
+ export interface Response {
+ data: ContentApiPermission;
+ }
+}
diff --git a/packages/core/admin/shared/contracts/content-api/routes.ts b/packages/core/admin/shared/contracts/content-api/routes.ts
new file mode 100644
index 0000000000..6ff8789439
--- /dev/null
+++ b/packages/core/admin/shared/contracts/content-api/routes.ts
@@ -0,0 +1,31 @@
+type Routes = Record<
+ string,
+ {
+ config: {
+ auth: {
+ scope: string[];
+ };
+ };
+ handler: string;
+ info: {
+ apiName: string;
+ type: string;
+ };
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE';
+ path: string;
+ }[]
+>;
+
+/**
+ * GET /content-api/routes - List content API routes
+ */
+export declare namespace List {
+ export interface Request {
+ body: {};
+ query: {};
+ }
+
+ export interface Response {
+ data: Routes;
+ }
+}
diff --git a/packages/core/admin/shared/contracts/user.ts b/packages/core/admin/shared/contracts/user.ts
index a8fb430902..05dfdac9f9 100644
--- a/packages/core/admin/shared/contracts/user.ts
+++ b/packages/core/admin/shared/contracts/user.ts
@@ -23,6 +23,21 @@ export declare namespace Create {
}
}
+/**
+ * /create - Create an admin user
+ */
+export declare namespace Create {
+ export interface Request {
+ body: AdminUserCreationPayload;
+ query: {};
+ }
+
+ export interface Response {
+ data: SanitizedAdminUser;
+ error?: errors.ApplicationError | errors.YupValidationError;
+ }
+}
+
/**
* /find - Find admin users
*/
diff --git a/packages/core/helper-plugin/src/features/Tracking.tsx b/packages/core/helper-plugin/src/features/Tracking.tsx
index 5fb5ee38f8..260fd3361d 100644
--- a/packages/core/helper-plugin/src/features/Tracking.tsx
+++ b/packages/core/helper-plugin/src/features/Tracking.tsx
@@ -211,8 +211,9 @@ interface WillNavigateEvent {
interface DidAccessTokenListEvent {
name: 'didAccessTokenList';
- properties: TokenEvents['properties'] & {
- number: string;
+ properties: {
+ tokenType: TokenEvents['properties']['tokenType'];
+ number: number;
};
}
interface LogoEvent {
@@ -225,15 +226,27 @@ interface LogoEvent {
interface TokenEvents {
name:
| 'didCopyTokenKey'
+ | 'didAddTokenFromList'
+ | 'didEditTokenFromList'
| 'willAccessTokenList'
| 'willAddTokenFromList'
+ | 'willCreateToken'
| 'willDeleteToken'
+ | 'willEditToken'
| 'willEditTokenFromList';
properties: {
- tokenType: string;
+ tokenType: 'api-token' | 'transfer-token';
};
}
+type WillModifyTokenEvent = {
+ name: 'didCreateToken' | 'didEditToken';
+ properties: {
+ tokenType: TokenEvents['properties']['tokenType'];
+ type: 'custom' | 'full-access' | 'read-only';
+ };
+};
+
type EventsWithProperties =
| DidAccessTokenListEvent
| DidChangeModeEvent
@@ -246,6 +259,7 @@ type EventsWithProperties =
| DidSubmitWithErrorsFirstAdminEvent
| LogoEvent
| TokenEvents
+ | WillModifyTokenEvent
| WillNavigateEvent
| DidSelectContentTypeFieldTypeEvent;