diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 937bb43b75..8c43ac13e1 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -129,6 +129,7 @@ module.exports = { '/3.0.0-beta.x/guides/deployment', '/3.0.0-beta.x/guides/jwt-validation', '/3.0.0-beta.x/guides/error-catching', + '/3.0.0-beta.x/guides/external-data', '/3.0.0-beta.x/guides/slug', '/3.0.0-beta.x/guides/webhooks', ], diff --git a/docs/3.0.0-beta.x/concepts/controllers.md b/docs/3.0.0-beta.x/concepts/controllers.md index b31a37552e..089838aa2a 100644 --- a/docs/3.0.0-beta.x/concepts/controllers.md +++ b/docs/3.0.0-beta.x/concepts/controllers.md @@ -67,7 +67,9 @@ module.exports = { entities = await strapi.services.restaurant.find(ctx.query); } - return entities.map(entity => sanitizeEntity(entity, { model })); + return entities.map(entity => + sanitizeEntity(entity, { model: strapi.models.restaurant }) + ); }, }; ``` @@ -90,7 +92,7 @@ module.exports = { async findOne(ctx) { const entity = await strapi.services.restaurant.findOne(ctx.params); - return sanitizeEntity(entity, { model }); + return sanitizeEntity(entity, { model: strapi.models.restaurant }); }, }; ``` @@ -142,7 +144,7 @@ module.exports = { } else { entity = await strapi.services.restaurant.create(ctx.request.body); } - return sanitizeEntity(entity, { model }); + return sanitizeEntity(entity, { model: strapi.models.restaurant }); }, }; ``` @@ -177,7 +179,7 @@ module.exports = { ); } - return sanitizeEntity(entity, { model }); + return sanitizeEntity(entity, { model: strapi.models.restaurant }); }, }; ``` @@ -200,7 +202,7 @@ module.exports = { async delete(ctx) { const entity = await strapi.services.restaurant.delete(ctx.params); - return sanitizeEntity(entity, { model }); + return sanitizeEntity(entity, { model: strapi.models.restaurant }); }, }; ``` diff --git a/docs/3.0.0-beta.x/concepts/services.md b/docs/3.0.0-beta.x/concepts/services.md index f7a5ed6985..1775336f1e 100644 --- a/docs/3.0.0-beta.x/concepts/services.md +++ b/docs/3.0.0-beta.x/concepts/services.md @@ -97,7 +97,7 @@ module.exports = { if (files) { // automatically uploads the files based on the entry and the model - await this.uploadFiles(entry, files, { model }); + await this.uploadFiles(entry, files, { model: strapi.models.restaurant }); return this.findOne({ id: entry.id }); } @@ -125,7 +125,7 @@ module.exports = { if (files) { // automatically uploads the files based on the entry and the model - await this.uploadFiles(entry, files, { model }); + await this.uploadFiles(entry, files, { model: strapi.models.restaurant }); return this.findOne({ id: entry.id }); } diff --git a/docs/3.0.0-beta.x/content-api/parameters.md b/docs/3.0.0-beta.x/content-api/parameters.md index 4862006e71..11d39ad0c2 100644 --- a/docs/3.0.0-beta.x/content-api/parameters.md +++ b/docs/3.0.0-beta.x/content-api/parameters.md @@ -89,7 +89,7 @@ Sort according to a specific field. - ASC: `GET /users?_sort=email:ASC` - DESC: `GET /users?_sort=email:DESC` -#### Sorting on multiple fileds +#### Sorting on multiple fields - `GET /users?_sort=email:asc,dateField:desc` - `GET /users?_sort=email:DESC,username:ASC` diff --git a/docs/3.0.0-beta.x/guides/external-data.md b/docs/3.0.0-beta.x/guides/external-data.md new file mode 100644 index 0000000000..e3497c6bec --- /dev/null +++ b/docs/3.0.0-beta.x/guides/external-data.md @@ -0,0 +1,96 @@ +# Fetching external data + +This guide explains how to fetch data from an external service to use it in your app. + +In this example we will see how to daily fetch Docker pull count to store the result in your database. + +## Content Type settings + +First, we need to create a Content Type, in this example we will call it `hit` with a `date` and `count` attribute. + +Your Content Type should look like this: + +**Path —** `./api/hit/models/Hit.settings.json` + +```json +{ + "connection": "default", + "collectionName": "hits", + "info": { + "name": "hit", + "description": "" + }, + "options": { + "increments": true, + "timestamps": true, + "comment": "" + }, + "attributes": { + "count": { + "type": "integer" + }, + "date": { + "type": "date" + } + } +} +``` + +## Fetch the data + +Now we will create a function that will be usable everywhere in your strapi application. + +**Path —** `./config/functions/docker.js` + +```js +const axios = require('axios'); + +module.exports = async () => { + const { data } = await axios.get( + 'https://hub.docker.com/v2/repositories/strapi/strapi/' + ); + + console.log(data); +}; +``` + +`data` contains all the data received from the Docker Hub API. What we want here is to add the `pull_count` value in your database. + +## Create a `hit` entry + +let's programmatically create the entry. + +**Path —** `./config/functions/docker.js` + +```js +const axios = require('axios'); + +module.exports = async () => { + const { data } = await axios.get( + 'https://hub.docker.com/v2/repositories/strapi/strapi/' + ); + + await strapi.query('hit').create({ + date: new Date(), + count: data.pull_count, + }); +}; +``` + +With this code, everytime this function is called it will fetch the docker repo's data and insert the current `pull_count` with the corresponding date in your Strapi database. + +## Call the function + +Here is how to call the function in your application `strapi.config.functions.docker()` + +So let's execute this function everyday at 2am. For this we will use a [CRON tasks](../concepts/configurations.md#cron-tasks). + +**Path —** `./config/functions/cron.js` + +```js +module.exports = { + '0 2 * * *': () => { + strapi.config.functions.docker(); + }, +}; +``` diff --git a/docs/3.0.0-beta.x/plugins/users-permissions.md b/docs/3.0.0-beta.x/plugins/users-permissions.md index 84dd43e8e3..b13322bd11 100644 --- a/docs/3.0.0-beta.x/plugins/users-permissions.md +++ b/docs/3.0.0-beta.x/plugins/users-permissions.md @@ -209,17 +209,42 @@ axios .post('http://localhost:1337/auth/reset-password', { code: 'privateCode', password: 'myNewPassword', - passwordConfirmation: 'myNewPassword' + passwordConfirmation: 'myNewPassword', }) .then(response => { // Handle success. - console.log('Your user\'s password has been changed.'); + console.log("Your user's password has been changed."); }) .catch(error => { // Handle error. console.log('An error occurred:', error); }); -}); +``` + +### Email validation + +This action send an email to a user with the link to confirm the user. + +#### Usage + +- email is the user email. + +```js +import axios from 'axios'; + +// Request API. +axios + .post(`http://localhost:1337/auth/send-email-confirmation`, { + email: 'user@strapi.io', + }) + .then(response => { + // Handle success. + console.log('Your user received an email'); + }) + .catch(error => { + // Handle error. + console.err('An error occured:', err); + }); ``` ## User object in Strapi context diff --git a/packages/strapi-admin/admin/src/containers/App/tests/index.test.js b/packages/strapi-admin/admin/src/containers/App/tests/index.test.js deleted file mode 100644 index 00797dda83..0000000000 --- a/packages/strapi-admin/admin/src/containers/App/tests/index.test.js +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import { LoadingIndicatorPage } from 'strapi-helper-plugin'; - -import { App } from '../../App'; - -describe('', () => { - it('should render the ', () => { - const renderedComponent = shallow(); - expect(renderedComponent.find(LoadingIndicatorPage)).toHaveLength(1); - }); -}); diff --git a/packages/strapi-generate-new/lib/create-customized-project.js b/packages/strapi-generate-new/lib/create-customized-project.js index 37a36138f2..448e04fbbd 100644 --- a/packages/strapi-generate-new/lib/create-customized-project.js +++ b/packages/strapi-generate-new/lib/create-customized-project.js @@ -44,7 +44,7 @@ async function askDbInfosAndTest(scope) { dependencies: clientDependencies({ scope, client }), }; - await testDatabaseConnection({ + return testDatabaseConnection({ scope, configuration, }) @@ -67,6 +67,7 @@ async function askDbInfosAndTest(scope) { }); } ) + .then(() => configuration) .catch(err => { if (retries < MAX_RETRIES - 1) { console.log(); @@ -88,8 +89,6 @@ async function askDbInfosAndTest(scope) { `️⛔️ Could not connect to your database after ${MAX_RETRIES} tries. Try to check your database configuration an retry.` ); }); - - return configuration; } return loop(); diff --git a/packages/strapi-generate-new/lib/resources/json/database.json.js b/packages/strapi-generate-new/lib/resources/json/database.json.js index 7e77fb10ad..b7a38e7c6b 100644 --- a/packages/strapi-generate-new/lib/resources/json/database.json.js +++ b/packages/strapi-generate-new/lib/resources/json/database.json.js @@ -3,24 +3,16 @@ module.exports = ({ connection, env }) => { // Production/Staging Template if (['production', 'staging'].includes(env)) { - // All available settings (bookshelf and mongoose) const settingsBase = { client: connection.settings.client, host: "${process.env.DATABASE_HOST || '127.0.0.1'}", port: '${process.env.DATABASE_PORT || 27017}', - srv: '${process.env.DATABASE_SRV || false}', database: "${process.env.DATABASE_NAME || 'strapi'}", username: "${process.env.DATABASE_USERNAME || ''}", password: "${process.env.DATABASE_PASSWORD || ''}", - ssl: '${process.env.DATABASE_SSL || false}', }; - // All available options (bookshelf and mongoose) - const optionsBase = { - ssl: '${process.env.DATABASE_SSL || false}', - authenticationDatabase: - "${process.env.DATABASE_AUTHENTICATION_DATABASE || ''}", - }; + const optionsBase = {}; return { defaultConnection: 'default', diff --git a/packages/strapi-helper-plugin/lib/src/components/InputsIndex/index.js b/packages/strapi-helper-plugin/lib/src/components/InputsIndex/index.js index 6558e1e2f4..4d5224ff96 100644 --- a/packages/strapi-helper-plugin/lib/src/components/InputsIndex/index.js +++ b/packages/strapi-helper-plugin/lib/src/components/InputsIndex/index.js @@ -6,7 +6,7 @@ /* eslint-disable react/require-default-props */ import React from 'react'; import PropTypes from 'prop-types'; -import { isEmpty, isObject, merge } from 'lodash'; +import { isEmpty, merge } from 'lodash'; // Design import InputAddonWithErrors from '../InputAddonWithErrors'; @@ -22,7 +22,11 @@ import InputTextAreaWithErrors from '../InputTextAreaWithErrors'; import InputTextWithErrors from '../InputTextWithErrors'; import InputToggleWithErrors from '../InputToggleWithErrors'; -const DefaultInputError = ({ type }) =>
Your input type: {type} does not exist
; +const DefaultInputError = ({ type }) => ( +
+ Your input type: {type} does not exist +
+); const inputs = { addon: InputAddonWithErrors, @@ -55,14 +59,14 @@ function InputsIndex(props) { inputValue = props.value || []; break; case 'json': - inputValue = isObject(props.value) ? props.value : null; + inputValue = props.value || null; break; default: inputValue = props.value || ''; } merge(inputs, props.customInputs); - + const Input = inputs[type] ? inputs[type] : DefaultInputError; return ; @@ -78,10 +82,7 @@ InputsIndex.defaultProps = { }; InputsIndex.propTypes = { - addon: PropTypes.oneOfType([ - PropTypes.bool, - PropTypes.string, - ]), + addon: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]), customInputs: PropTypes.object, type: PropTypes.string.isRequired, value: PropTypes.any, diff --git a/packages/strapi-plugin-content-manager/admin/src/components/InputJSON/index.js b/packages/strapi-plugin-content-manager/admin/src/components/InputJSON/index.js index 64da0dcb49..cf58c08923 100644 --- a/packages/strapi-plugin-content-manager/admin/src/components/InputJSON/index.js +++ b/packages/strapi-plugin-content-manager/admin/src/components/InputJSON/index.js @@ -15,13 +15,12 @@ import 'codemirror/addon/selection/mark-selection'; import 'codemirror/lib/codemirror.css'; import 'codemirror/theme/3024-night.css'; -import { isEmpty, isObject, trimStart } from 'lodash'; +import { isEmpty, trimStart } from 'lodash'; import jsonlint from './jsonlint'; import Wrapper from './components'; const WAIT = 600; const stringify = JSON.stringify; -const parse = JSON.parse; const DEFAULT_THEME = '3024-night'; class InputJSON extends React.Component { @@ -65,15 +64,14 @@ class InputJSON extends React.Component { setInitValue = () => { const { value } = this.props; - if (isObject(value) && value !== null) { - try { - parse(stringify(value)); - this.setState({ hasInitValue: true }); + try { + this.setState({ hasInitValue: true }); - return this.codeMirror.setValue(stringify(value, null, 2)); - } catch (err) { - return this.setState({ error: true }); - } + if (value === null) return this.codeMirror.setValue(''); + + return this.codeMirror.setValue(stringify(value, null, 2)); + } catch (err) { + return this.setState({ error: true }); } }; @@ -125,10 +123,8 @@ class InputJSON extends React.Component { const { name, onChange } = this.props; let value = this.codeMirror.getValue(); - try { - value = parse(value); - } catch (err) { - // Silent + if (value === '') { + value = null; } // Update the parent diff --git a/packages/strapi-plugin-content-manager/admin/src/components/InputJSONWithErrors/index.js b/packages/strapi-plugin-content-manager/admin/src/components/InputJSONWithErrors/index.js index e022848607..1d0a2cae27 100644 --- a/packages/strapi-plugin-content-manager/admin/src/components/InputJSONWithErrors/index.js +++ b/packages/strapi-plugin-content-manager/admin/src/components/InputJSONWithErrors/index.js @@ -217,11 +217,7 @@ InputJSONWithErrors.propTypes = { resetProps: PropTypes.bool, tabIndex: PropTypes.string, validations: PropTypes.object, - value: PropTypes.oneOfType([ - PropTypes.array, - PropTypes.object, - PropTypes.bool, - ]), + value: PropTypes.any, }; export default InputJSONWithErrors; diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/EditView/utils/formatData.js b/packages/strapi-plugin-content-manager/admin/src/containers/EditView/utils/formatData.js index 9ed0af6ca4..f6904897d8 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/EditView/utils/formatData.js +++ b/packages/strapi-plugin-content-manager/admin/src/containers/EditView/utils/formatData.js @@ -19,7 +19,7 @@ export const cleanData = (retrievedData, ctLayout, groupLayouts) => { switch (attrType) { case 'json': - cleanedData = value; + cleanedData = JSON.parse(value); break; case 'date': cleanedData = diff --git a/packages/strapi-plugin-content-manager/admin/src/containers/EditView/utils/schema.js b/packages/strapi-plugin-content-manager/admin/src/containers/EditView/utils/schema.js index 598298c225..aab240b62e 100644 --- a/packages/strapi-plugin-content-manager/admin/src/containers/EditView/utils/schema.js +++ b/packages/strapi-plugin-content-manager/admin/src/containers/EditView/utils/schema.js @@ -1,12 +1,4 @@ -import { - get, - isBoolean, - isNaN, - isNumber, - isNull, - isArray, - isObject, -} from 'lodash'; +import { get, isBoolean, isNaN } from 'lodash'; import * as yup from 'yup'; import { translatedErrors as errorsTrads } from 'strapi-helper-plugin'; @@ -99,20 +91,13 @@ const createYupSchemaAttribute = (type, validations) => { schema = yup .mixed(errorsTrads.json) .test('isJSON', errorsTrads.json, value => { - try { - if ( - isObject(value) || - isBoolean(value) || - isNumber(value) || - isArray(value) || - isNaN(value) || - isNull(value) - ) { - JSON.parse(JSON.stringify(value)); - return true; - } + if (value === undefined) { + return true; + } - return false; + try { + JSON.parse(value); + return true; } catch (err) { return false; } diff --git a/packages/strapi-plugin-graphql/services/Aggregator.js b/packages/strapi-plugin-graphql/services/Aggregator.js index 6fffd05d0b..09a9139fa3 100644 --- a/packages/strapi-plugin-graphql/services/Aggregator.js +++ b/packages/strapi-plugin-graphql/services/Aggregator.js @@ -358,12 +358,12 @@ const formatConnectionAggregator = function(fields, model, modelName) { if (opts._q) { // allow search param - return strapi.query(modelName).countSearch(opts); + return strapi.query(modelName, model.plugin).countSearch(opts); } - return strapi.query(modelName).count(opts); + return strapi.query(modelName, model.plugin).count(opts); }, totalCount(obj, options, context) { - return strapi.query(modelName).count({}); + return strapi.query(modelName, model.plugin).count({}); }, }, }; diff --git a/packages/strapi-plugin-users-permissions/config/routes.json b/packages/strapi-plugin-users-permissions/config/routes.json index 77204bb0f2..94b5c53a63 100644 --- a/packages/strapi-plugin-users-permissions/config/routes.json +++ b/packages/strapi-plugin-users-permissions/config/routes.json @@ -285,6 +285,20 @@ } } }, + { + "method": "POST", + "path": "/auth/send-email-confirmation", + "handler": "Auth.sendEmailConfirmation", + "config": { + "policies": [], + "prefix": "", + "description": "Send a confirmation email to user", + "tag": { + "plugin": "users-permissions", + "name": "User" + } + } + }, { "method": "GET", "path": "/users", diff --git a/packages/strapi-plugin-users-permissions/controllers/Auth.js b/packages/strapi-plugin-users-permissions/controllers/Auth.js index c3e56e5b55..655b3b9230 100644 --- a/packages/strapi-plugin-users-permissions/controllers/Auth.js +++ b/packages/strapi-plugin-users-permissions/controllers/Auth.js @@ -597,4 +597,80 @@ module.exports = { ctx.redirect(settings.email_confirmation_redirection || '/'); }, + + async sendEmailConfirmation(ctx) { + const pluginStore = await strapi.store({ + environment: '', + type: 'plugin', + name: 'users-permissions', + }); + + const params = _.assign(ctx.request.body); + + if (!params.email) { + return ctx.badRequest('missing.email'); + } + + const isEmail = emailRegExp.test(params.email); + + if (isEmail) { + params.email = params.email.toLowerCase(); + } else { + return ctx.badRequest('wrong.email'); + } + + const user = await strapi.query('user', 'users-permissions').findOne({ + email: params.email + }); + + if (user.confirmed) { + return ctx.badRequest('already.confirmed'); + } + + if (user.blocked) { + return ctx.badRequest('blocked.user'); + } + + const jwt = strapi.plugins['users-permissions'].services.jwt.issue( + _.pick(user.toJSON ? user.toJSON() : user, ['id']) + ); + + const settings = await pluginStore.get({ key: 'email' }).then(storeEmail => { + try { + return storeEmail['email_confirmation'].options; + } catch (err) { + return {}; + } + }); + + settings.message = await strapi.plugins['users-permissions'].services.userspermissions.template(settings.message, { + URL: new URL('/auth/email-confirmation', strapi.config.url).toString(), + USER: _.omit(user.toJSON ? user.toJSON() : user, ['password', 'resetPasswordToken', 'role', 'provider']), + CODE: jwt + }); + + settings.object = await strapi.plugins['users-permissions'].services.userspermissions.template(settings.object, { + USER: _.omit(user.toJSON ? user.toJSON() : user, ['password', 'resetPasswordToken', 'role', 'provider']), + }); + + try { + await strapi.plugins['email'].services.email.send({ + to: (user.toJSON ? user.toJSON() : user).email, + from: + settings.from.email && settings.from.name + ? `"${settings.from.name}" <${settings.from.email}>` + : undefined, + replyTo: settings.response_email, + subject: settings.object, + text: settings.message, + html: settings.message + }); + ctx.send({ + email: (user.toJSON ? user.toJSON() : user).email, + sent: true + }); + } catch (err) { + return ctx.badRequest(null, err); + } + }, }; diff --git a/packages/strapi-plugin-users-permissions/documentation/1.0.0/overrides/users-permissions-User.json b/packages/strapi-plugin-users-permissions/documentation/1.0.0/overrides/users-permissions-User.json index c6d0821c09..3b55f96ae0 100644 --- a/packages/strapi-plugin-users-permissions/documentation/1.0.0/overrides/users-permissions-User.json +++ b/packages/strapi-plugin-users-permissions/documentation/1.0.0/overrides/users-permissions-User.json @@ -53,6 +53,47 @@ "security": [] } }, + "/auth/send-email-confirmation": { + "post": { + "security": [], + "externalDocs": { + "description": "Find out more in the strapi's documentation", + "url": "https://strapi.io/documentation/guides/authentication.html#usage" + }, + "responses": { + "200": { + "description": "Successfully sent email", + "content": { + "application/json": { + "email": { + "type": "string" + }, + "sent": { + "type": "boolean" + } + } + } + } + }, + "requestBody": { + "description": "", + "required": true, + "content": { + "application/json": { + "schema": { + "required": ["email"], + "properties": { + "email": { + "type": "string", + "minLength": 6 + } + } + } + } + } + } + } + }, "/users-permissions/search/{id}": { "get": { "summary": "Retrieve a list of users by searching for their username or email", diff --git a/packages/strapi-plugin-users-permissions/services/UsersPermissions.js b/packages/strapi-plugin-users-permissions/services/UsersPermissions.js index e630315fa4..52b2c17efa 100644 --- a/packages/strapi-plugin-users-permissions/services/UsersPermissions.js +++ b/packages/strapi-plugin-users-permissions/services/UsersPermissions.js @@ -389,7 +389,9 @@ module.exports = { }; // Retrieve roles - const roles = await strapi.query('role', 'users-permissions').find(); + const roles = await strapi + .query('role', 'users-permissions') + .find({}, []); // We have to know the difference to add or remove // the permissions entries in the database.