mirror of
https://github.com/strapi/strapi.git
synced 2025-11-02 10:55:37 +00:00
Merge branch 'main' into bagfix/15308
This commit is contained in:
commit
17a547e78f
103
docs/docs/core/hooks/use-fetch-client.mdx
Normal file
103
docs/docs/core/hooks/use-fetch-client.mdx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
---
|
||||||
|
title: useFetchClient
|
||||||
|
slug: /hooks/use-fetch-client
|
||||||
|
description: API reference for the useFetchClient hook in Strapi
|
||||||
|
tags:
|
||||||
|
- hooks
|
||||||
|
- axios
|
||||||
|
- data
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
The following example shows a basic way to use the `useFetchClient` hook to make a get request to a Strapi backend endpoint:
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
import {useState} from "react"
|
||||||
|
import useFetchClient from '@strapi/admin/admin/src/hooks/useFetchClient';
|
||||||
|
|
||||||
|
const Component = () => {
|
||||||
|
const [items, setItems] = useState([]);
|
||||||
|
const { get } = useFetchClient();
|
||||||
|
const requestURL = "/some-endpoint";
|
||||||
|
|
||||||
|
const handleGetData = async () => {
|
||||||
|
const { data } = await get(requestURL);
|
||||||
|
setItems(data.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
return(
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
items && items.map(item => <h2 key={item.uuid}>{item.name}</h2>))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<button onClick={handleGetData}>Show Items</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Methods
|
||||||
|
|
||||||
|
Essentially, this is an abstraction around the axios instance exposed by a hook. It provides a simple interface to handle API calls to the Strapi backend.
|
||||||
|
It handles request cancellations inside the hook with an [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController). This is typically triggered when the component is unmounted so all the requests that it is currently making are aborted.
|
||||||
|
|
||||||
|
The hook exposes four methods:
|
||||||
|
|
||||||
|
- **get(url, config)**: requires a relative url (`string`) and makes a `GET` request to the Strapi backend with an optional configuration `object` passed as second parameter.
|
||||||
|
- **post(url, data, config)**: requires a relative url (`string`) and makes a `POST` request with the required data `object` to the Strapi backend with the optional configuration `object` passed as third parameter.
|
||||||
|
- **put(url, data, config)**: requires a relative url (`string`) and makes a `PUT` request with the required data `object` to the Strapi backend with the optional configuration `object` passed as third parameter.
|
||||||
|
- **del(url, config)**: requires a relative url (`string`) and makes a `DELETE` request to the Strapi backend with an optional configuration `object` passed as second parameter.
|
||||||
|
|
||||||
|
## Implementation details
|
||||||
|
|
||||||
|
The following information is the internal additions we've added to the axios instance via two request interceptors. As well as an explanation of the `baseUrl`.
|
||||||
|
|
||||||
|
### Base URL
|
||||||
|
|
||||||
|
The default URL will be the one defined in the environment variable: `STRAPI_ADMIN_BACKEND_URL`.
|
||||||
|
|
||||||
|
### Interceptors
|
||||||
|
|
||||||
|
#### Request
|
||||||
|
|
||||||
|
The request interceptor adds the following parameters to the header:
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
Authorization: `Bearer <AUTH_TOKEN>`,
|
||||||
|
Accept: 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
If everything works correctly, the response is returned as it comes from the backend. However, if it contains a **status code of 401** the authentication details will be removed from
|
||||||
|
the application storage and the window will be reloaded.
|
||||||
|
|
||||||
|
:::caution
|
||||||
|
Have this in mind if using this hook in pages where the auth token is not available, such as `login`, because it will create an infinite loop. See the [Troubleshooting](#troubleshooting) section for more information.
|
||||||
|
:::
|
||||||
|
|
||||||
|
## Further Reading
|
||||||
|
|
||||||
|
- [axios instance API](https://axios-http.com/docs/instance)
|
||||||
|
- [AbortController](https://axios-http.com/docs/cancellation)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Authentication problems
|
||||||
|
|
||||||
|
Trying to access a protected route from a context where the auth token is not available may lead to an infinite loop due to the response interceptor that
|
||||||
|
reloads the page when obtaining a 401 response. One option to avoid this from happening is to not consider a 401 response as an error, see below:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const {
|
||||||
|
data: { data: properties },
|
||||||
|
} = await get(`/protected-endpoint`, {
|
||||||
|
validateStatus: (status) => status < 500,
|
||||||
|
});
|
||||||
|
```
|
||||||
@ -39,6 +39,17 @@ const sidebars = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'category',
|
||||||
|
label: 'Hooks',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'doc',
|
||||||
|
label: 'useFetchClient',
|
||||||
|
id: 'core/hooks/use-fetch-client',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: 'category',
|
type: 'category',
|
||||||
label: 'Content Type Builder',
|
label: 'Content Type Builder',
|
||||||
|
|||||||
@ -22,7 +22,9 @@ import getTrad from '../../../utils/getTrad';
|
|||||||
import { makeSelectModelLinks } from '../selectors';
|
import { makeSelectModelLinks } from '../selectors';
|
||||||
|
|
||||||
const matchByTitle = (links, search) =>
|
const matchByTitle = (links, search) =>
|
||||||
matchSorter(links, toLower(search), { keys: [(item) => toLower(item.title)] });
|
search
|
||||||
|
? matchSorter(links, toLower(search), { keys: [(item) => toLower(item.title)] })
|
||||||
|
: sortBy(links, (object) => object.title.toLowerCase());
|
||||||
|
|
||||||
const LeftMenu = () => {
|
const LeftMenu = () => {
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
@ -52,9 +54,7 @@ const LeftMenu = () => {
|
|||||||
defaultMessage: 'Collection Types',
|
defaultMessage: 'Collection Types',
|
||||||
},
|
},
|
||||||
searchable: true,
|
searchable: true,
|
||||||
links: sortBy(matchByTitle(intlCollectionTypeLinks, search), (object) =>
|
links: matchByTitle(intlCollectionTypeLinks, search),
|
||||||
object.title.toLowerCase()
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'singleTypes',
|
id: 'singleTypes',
|
||||||
@ -63,9 +63,7 @@ const LeftMenu = () => {
|
|||||||
defaultMessage: 'Single Types',
|
defaultMessage: 'Single Types',
|
||||||
},
|
},
|
||||||
searchable: true,
|
searchable: true,
|
||||||
links: sortBy(matchByTitle(intlSingleTypeLinks, search), (object) =>
|
links: matchByTitle(intlSingleTypeLinks, search),
|
||||||
object.title.toLowerCase()
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -162,7 +162,7 @@ const useContentTypeBuilderMenu = () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const matchByTitle = (links) =>
|
const matchByTitle = (links) =>
|
||||||
matchSorter(links, toLower(search), { keys: [(item) => toLower(item.title)] });
|
search ? matchSorter(links, toLower(search), { keys: [(item) => toLower(item.title)] }) : links;
|
||||||
|
|
||||||
const getMenu = () => {
|
const getMenu = () => {
|
||||||
// Maybe we can do it simpler with matchsorter wildcards ?
|
// Maybe we can do it simpler with matchsorter wildcards ?
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import { ModalLayout, ModalBody, ModalFooter } from '@strapi/design-system/Modal
|
|||||||
import { Tabs, Tab, TabGroup, TabPanels, TabPanel } from '@strapi/design-system/Tabs';
|
import { Tabs, Tab, TabGroup, TabPanels, TabPanel } from '@strapi/design-system/Tabs';
|
||||||
import { Flex } from '@strapi/design-system/Flex';
|
import { Flex } from '@strapi/design-system/Flex';
|
||||||
import { Stack } from '@strapi/design-system/Stack';
|
import { Stack } from '@strapi/design-system/Stack';
|
||||||
|
import { isEqual } from 'lodash';
|
||||||
import pluginId from '../../pluginId';
|
import pluginId from '../../pluginId';
|
||||||
import useDataManager from '../../hooks/useDataManager';
|
import useDataManager from '../../hooks/useDataManager';
|
||||||
import useFormModalNavigation from '../../hooks/useFormModalNavigation';
|
import useFormModalNavigation from '../../hooks/useFormModalNavigation';
|
||||||
@ -790,13 +791,35 @@ const FormModal = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleConfirmClose = () => {
|
||||||
|
// eslint-disable-next-line no-alert
|
||||||
|
const confirm = window.confirm(
|
||||||
|
formatMessage({
|
||||||
|
id: 'window.confirm.close-modal.file',
|
||||||
|
defaultMessage: 'Are you sure? Your changes will be lost.',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirm) {
|
||||||
|
onCloseModal();
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: RESET_PROPS,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleClosed = () => {
|
const handleClosed = () => {
|
||||||
// Close the modal
|
// Close the modal
|
||||||
onCloseModal();
|
if (!isEqual(modifiedData, initialData)) {
|
||||||
// Reset the reducer
|
handleConfirmClose();
|
||||||
dispatch({
|
} else {
|
||||||
type: RESET_PROPS,
|
onCloseModal();
|
||||||
});
|
// Reset the reducer
|
||||||
|
dispatch({
|
||||||
|
type: RESET_PROPS,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendAdvancedTabEvent = (tab) => {
|
const sendAdvancedTabEvent = (tab) => {
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
const {
|
||||||
|
template: { createStrictInterpolationRegExp },
|
||||||
|
keysDeep,
|
||||||
|
} = require('@strapi/utils/');
|
||||||
|
|
||||||
const getProviderSettings = () => {
|
const getProviderSettings = () => {
|
||||||
return strapi.config.get('plugin.email');
|
return strapi.config.get('plugin.email');
|
||||||
@ -26,10 +30,19 @@ const sendTemplatedEmail = (emailOptions = {}, emailTemplate = {}, data = {}) =>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const allowedInterpolationVariables = keysDeep(data);
|
||||||
|
const interpolate = createStrictInterpolationRegExp(allowedInterpolationVariables, 'g');
|
||||||
|
|
||||||
const templatedAttributes = attributes.reduce(
|
const templatedAttributes = attributes.reduce(
|
||||||
(compiled, attribute) =>
|
(compiled, attribute) =>
|
||||||
emailTemplate[attribute]
|
emailTemplate[attribute]
|
||||||
? Object.assign(compiled, { [attribute]: _.template(emailTemplate[attribute])(data) })
|
? Object.assign(compiled, {
|
||||||
|
[attribute]: _.template(emailTemplate[attribute], {
|
||||||
|
interpolate,
|
||||||
|
evaluate: false,
|
||||||
|
escape: false,
|
||||||
|
})(data),
|
||||||
|
})
|
||||||
: compiled,
|
: compiled,
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|||||||
@ -24,7 +24,7 @@ const {
|
|||||||
joinBy,
|
joinBy,
|
||||||
toKebabCase,
|
toKebabCase,
|
||||||
} = require('./string-formatting');
|
} = require('./string-formatting');
|
||||||
const { removeUndefined } = require('./object-formatting');
|
const { removeUndefined, keysDeep } = require('./object-formatting');
|
||||||
const { getConfigUrls, getAbsoluteAdminUrl, getAbsoluteServerUrl } = require('./config');
|
const { getConfigUrls, getAbsoluteAdminUrl, getAbsoluteServerUrl } = require('./config');
|
||||||
const { generateTimestampCode } = require('./code-generator');
|
const { generateTimestampCode } = require('./code-generator');
|
||||||
const contentTypes = require('./content-types');
|
const contentTypes = require('./content-types');
|
||||||
@ -40,6 +40,7 @@ const traverseEntity = require('./traverse-entity');
|
|||||||
const pipeAsync = require('./pipe-async');
|
const pipeAsync = require('./pipe-async');
|
||||||
const convertQueryParams = require('./convert-query-params');
|
const convertQueryParams = require('./convert-query-params');
|
||||||
const importDefault = require('./import-default');
|
const importDefault = require('./import-default');
|
||||||
|
const template = require('./template');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
yup,
|
yup,
|
||||||
@ -61,11 +62,13 @@ module.exports = {
|
|||||||
getConfigUrls,
|
getConfigUrls,
|
||||||
escapeQuery,
|
escapeQuery,
|
||||||
removeUndefined,
|
removeUndefined,
|
||||||
|
keysDeep,
|
||||||
getAbsoluteAdminUrl,
|
getAbsoluteAdminUrl,
|
||||||
getAbsoluteServerUrl,
|
getAbsoluteServerUrl,
|
||||||
generateTimestampCode,
|
generateTimestampCode,
|
||||||
stringIncludes,
|
stringIncludes,
|
||||||
stringEquals,
|
stringEquals,
|
||||||
|
template,
|
||||||
isKebabCase,
|
isKebabCase,
|
||||||
isCamelCase,
|
isCamelCase,
|
||||||
toKebabCase,
|
toKebabCase,
|
||||||
|
|||||||
@ -4,6 +4,12 @@ const _ = require('lodash');
|
|||||||
|
|
||||||
const removeUndefined = (obj) => _.pickBy(obj, (value) => typeof value !== 'undefined');
|
const removeUndefined = (obj) => _.pickBy(obj, (value) => typeof value !== 'undefined');
|
||||||
|
|
||||||
|
const keysDeep = (obj, path = []) =>
|
||||||
|
!_.isObject(obj)
|
||||||
|
? path.join('.')
|
||||||
|
: _.reduce(obj, (acc, next, key) => _.concat(acc, keysDeep(next, [...path, key])), []);
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
removeUndefined,
|
removeUndefined,
|
||||||
|
keysDeep,
|
||||||
};
|
};
|
||||||
|
|||||||
28
packages/core/utils/lib/template.js
Normal file
28
packages/core/utils/lib/template.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a strict interpolation RegExp based on the given variables' name
|
||||||
|
*
|
||||||
|
* @param {string[]} allowedVariableNames - The list of allowed variables
|
||||||
|
* @param {string} [flags] - The RegExp flags
|
||||||
|
*/
|
||||||
|
const createStrictInterpolationRegExp = (allowedVariableNames, flags) => {
|
||||||
|
const oneOfVariables = allowedVariableNames.join('|');
|
||||||
|
|
||||||
|
// 1. We need to match the delimiters: <%= ... %>
|
||||||
|
// 2. We accept any number of whitespaces characters before and/or after the variable name: \s* ... \s*
|
||||||
|
// 3. We only accept values from the variable list as interpolation variables' name: : (${oneOfVariables})
|
||||||
|
return new RegExp(`<%=\\s*(${oneOfVariables})\\s*%>`, flags);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a loose interpolation RegExp to match as many groups as possible
|
||||||
|
*
|
||||||
|
* @param {string} [flags] - The RegExp flags
|
||||||
|
*/
|
||||||
|
const createLooseInterpolationRegExp = (flags) => new RegExp(/<%=([\s\S]+?)%>/, flags);
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createStrictInterpolationRegExp,
|
||||||
|
createLooseInterpolationRegExp,
|
||||||
|
};
|
||||||
@ -17,6 +17,11 @@ describe('isValidEmailTemplate', () => {
|
|||||||
expect(isValidEmailTemplate('<%CODE%>')).toBe(false);
|
expect(isValidEmailTemplate('<%CODE%>')).toBe(false);
|
||||||
expect(isValidEmailTemplate('${CODE}')).toBe(false);
|
expect(isValidEmailTemplate('${CODE}')).toBe(false);
|
||||||
expect(isValidEmailTemplate('${ CODE }')).toBe(false);
|
expect(isValidEmailTemplate('${ CODE }')).toBe(false);
|
||||||
|
expect(
|
||||||
|
isValidEmailTemplate(
|
||||||
|
'<%=`${ console.log({ "remote-execution": { "foo": "bar" }/*<>%=*/ }) }`%>'
|
||||||
|
)
|
||||||
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Fails on non authorized keys', () => {
|
test('Fails on non authorized keys', () => {
|
||||||
|
|||||||
@ -1,8 +1,17 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const _ = require('lodash');
|
const { trim } = require('lodash/fp');
|
||||||
|
const {
|
||||||
|
template: { createLooseInterpolationRegExp, createStrictInterpolationRegExp },
|
||||||
|
} = require('@strapi/utils');
|
||||||
|
|
||||||
|
const invalidPatternsRegexes = [
|
||||||
|
// Ignore "evaluation" patterns: <% ... %>
|
||||||
|
/<%[^=]([\s\S]*?)%>/m,
|
||||||
|
// Ignore basic string interpolations
|
||||||
|
/\${([^{}]*)}/m,
|
||||||
|
];
|
||||||
|
|
||||||
const invalidPatternsRegexes = [/<%[^=]([^<>%]*)%>/m, /\${([^{}]*)}/m];
|
|
||||||
const authorizedKeys = [
|
const authorizedKeys = [
|
||||||
'URL',
|
'URL',
|
||||||
'ADMIN_URL',
|
'ADMIN_URL',
|
||||||
@ -19,27 +28,42 @@ const matchAll = (pattern, src) => {
|
|||||||
let match;
|
let match;
|
||||||
|
|
||||||
const regexPatternWithGlobal = RegExp(pattern, 'g');
|
const regexPatternWithGlobal = RegExp(pattern, 'g');
|
||||||
|
|
||||||
// eslint-disable-next-line no-cond-assign
|
// eslint-disable-next-line no-cond-assign
|
||||||
while ((match = regexPatternWithGlobal.exec(src))) {
|
while ((match = regexPatternWithGlobal.exec(src))) {
|
||||||
const [, group] = match;
|
const [, group] = match;
|
||||||
|
|
||||||
matches.push(_.trim(group));
|
matches.push(trim(group));
|
||||||
}
|
}
|
||||||
|
|
||||||
return matches;
|
return matches;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isValidEmailTemplate = (template) => {
|
const isValidEmailTemplate = (template) => {
|
||||||
|
// Check for known invalid patterns
|
||||||
for (const reg of invalidPatternsRegexes) {
|
for (const reg of invalidPatternsRegexes) {
|
||||||
if (reg.test(template)) {
|
if (reg.test(template)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const matches = matchAll(/<%=([^<>%=]*)%>/, template);
|
const interpolation = {
|
||||||
for (const match of matches) {
|
// Strict interpolation pattern to match only valid groups
|
||||||
if (!authorizedKeys.includes(match)) {
|
strict: createStrictInterpolationRegExp(authorizedKeys),
|
||||||
return false;
|
// Weak interpolation pattern to match as many group as possible.
|
||||||
}
|
loose: createLooseInterpolationRegExp(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Compute both strict & loose matches
|
||||||
|
const strictMatches = matchAll(interpolation.strict, template);
|
||||||
|
const looseMatches = matchAll(interpolation.loose, template);
|
||||||
|
|
||||||
|
// If we have more matches with the loose RegExp than with the strict one,
|
||||||
|
// then it means that at least one of the interpolation group is invalid
|
||||||
|
// Note: In the future, if we wanted to give more details for error formatting
|
||||||
|
// purposes, we could return the difference between the two arrays
|
||||||
|
if (looseMatches.length > strictMatches.length) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@ -109,17 +109,25 @@ module.exports = ({ strapi }) => ({
|
|||||||
await this.edit(user.id, { confirmationToken });
|
await this.edit(user.id, { confirmationToken });
|
||||||
|
|
||||||
const apiPrefix = strapi.config.get('api.rest.prefix');
|
const apiPrefix = strapi.config.get('api.rest.prefix');
|
||||||
settings.message = await userPermissionService.template(settings.message, {
|
|
||||||
URL: urlJoin(getAbsoluteServerUrl(strapi.config), apiPrefix, '/auth/email-confirmation'),
|
|
||||||
SERVER_URL: getAbsoluteServerUrl(strapi.config),
|
|
||||||
ADMIN_URL: getAbsoluteAdminUrl(strapi.config),
|
|
||||||
USER: sanitizedUserInfo,
|
|
||||||
CODE: confirmationToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
settings.object = await userPermissionService.template(settings.object, {
|
try {
|
||||||
USER: sanitizedUserInfo,
|
settings.message = await userPermissionService.template(settings.message, {
|
||||||
});
|
URL: urlJoin(getAbsoluteServerUrl(strapi.config), apiPrefix, '/auth/email-confirmation'),
|
||||||
|
SERVER_URL: getAbsoluteServerUrl(strapi.config),
|
||||||
|
ADMIN_URL: getAbsoluteAdminUrl(strapi.config),
|
||||||
|
USER: sanitizedUserInfo,
|
||||||
|
CODE: confirmationToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
settings.object = await userPermissionService.template(settings.object, {
|
||||||
|
USER: sanitizedUserInfo,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
strapi.log.error(
|
||||||
|
'[plugin::users-permissions.sendConfirmationEmail]: Failed to generate a template for "user confirmation email". Please make sure your email template is valid and does not contain invalid characters or patterns'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Send an email to the user.
|
// Send an email to the user.
|
||||||
await strapi
|
await strapi
|
||||||
|
|||||||
@ -3,6 +3,11 @@
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const { filter, map, pipe, prop } = require('lodash/fp');
|
const { filter, map, pipe, prop } = require('lodash/fp');
|
||||||
const urlJoin = require('url-join');
|
const urlJoin = require('url-join');
|
||||||
|
const {
|
||||||
|
template: { createStrictInterpolationRegExp },
|
||||||
|
errors,
|
||||||
|
keysDeep,
|
||||||
|
} = require('@strapi/utils');
|
||||||
|
|
||||||
const { getService } = require('../utils');
|
const { getService } = require('../utils');
|
||||||
|
|
||||||
@ -230,7 +235,15 @@ module.exports = ({ strapi }) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
template(layout, data) {
|
template(layout, data) {
|
||||||
const compiledObject = _.template(layout);
|
const allowedTemplateVariables = keysDeep(data);
|
||||||
return compiledObject(data);
|
|
||||||
|
// Create a strict interpolation RegExp based on possible variable names
|
||||||
|
const interpolate = createStrictInterpolationRegExp(allowedTemplateVariables, 'g');
|
||||||
|
|
||||||
|
try {
|
||||||
|
return _.template(layout, { interpolate, evaluate: false, escape: false })(data);
|
||||||
|
} catch (e) {
|
||||||
|
throw new errors.ApplicationError('Invalid email template');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -37,7 +37,7 @@
|
|||||||
"test": "echo \"no tests yet\""
|
"test": "echo \"no tests yet\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"aws-sdk": "2.1260.0",
|
"aws-sdk": "2.1287.0",
|
||||||
"lodash": "4.17.21"
|
"lodash": "4.17.21"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
35
yarn.lock
35
yarn.lock
@ -7792,10 +7792,10 @@ available-typed-arrays@^1.0.5:
|
|||||||
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
|
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
|
||||||
integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
|
integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==
|
||||||
|
|
||||||
aws-sdk@2.1260.0:
|
aws-sdk@2.1287.0:
|
||||||
version "2.1260.0"
|
version "2.1287.0"
|
||||||
resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1260.0.tgz#3dc18f49dd4aaa18e4e9787c62f1ad02e5aaac4c"
|
resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1287.0.tgz#40296aadf34e7550e058f73f3363130d65ffb1c0"
|
||||||
integrity sha512-iciXVukPbhmh44xcF+5/CO15jtESqRkXuEH54XaU8IpCzbYkAcPBaS29vLRN2SRuN1Dy2S3X7SaZZxFJWLAHrg==
|
integrity sha512-mtfDstUdFNn8FnBaXs2KAaQ0cgDIiwlqwC2UptUKWWrugjZHAoRacfD/6bnah1Kwhu43F9CDEe5QLHnQtymNkw==
|
||||||
dependencies:
|
dependencies:
|
||||||
buffer "4.9.2"
|
buffer "4.9.2"
|
||||||
events "1.1.1"
|
events "1.1.1"
|
||||||
@ -15014,9 +15014,9 @@ json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1:
|
|||||||
integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==
|
integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==
|
||||||
|
|
||||||
json5@^1.0.1:
|
json5@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
|
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
|
||||||
integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==
|
integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==
|
||||||
dependencies:
|
dependencies:
|
||||||
minimist "^1.2.0"
|
minimist "^1.2.0"
|
||||||
|
|
||||||
@ -16463,12 +16463,7 @@ minimist-options@4.1.0:
|
|||||||
is-plain-obj "^1.1.0"
|
is-plain-obj "^1.1.0"
|
||||||
kind-of "^6.0.3"
|
kind-of "^6.0.3"
|
||||||
|
|
||||||
minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6:
|
minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6:
|
||||||
version "1.2.6"
|
|
||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
|
||||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
|
||||||
|
|
||||||
minimist@^1.2.0:
|
|
||||||
version "1.2.7"
|
version "1.2.7"
|
||||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
|
||||||
integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
|
integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
|
||||||
@ -22367,19 +22362,7 @@ util@^0.11.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
inherits "2.0.3"
|
inherits "2.0.3"
|
||||||
|
|
||||||
util@^0.12.0, util@^0.12.4:
|
util@^0.12.0, util@^0.12.3, util@^0.12.4:
|
||||||
version "0.12.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/util/-/util-0.12.4.tgz#66121a31420df8f01ca0c464be15dfa1d1850253"
|
|
||||||
integrity sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==
|
|
||||||
dependencies:
|
|
||||||
inherits "^2.0.3"
|
|
||||||
is-arguments "^1.0.4"
|
|
||||||
is-generator-function "^1.0.7"
|
|
||||||
is-typed-array "^1.1.3"
|
|
||||||
safe-buffer "^5.1.2"
|
|
||||||
which-typed-array "^1.1.2"
|
|
||||||
|
|
||||||
util@^0.12.3:
|
|
||||||
version "0.12.5"
|
version "0.12.5"
|
||||||
resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc"
|
resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc"
|
||||||
integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==
|
integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user