diff --git a/.github/ISSUE_TEMPLATE/SECURITY.md b/.github/ISSUE_TEMPLATE/SECURITY.md deleted file mode 100644 index 10cc026edb..0000000000 --- a/.github/ISSUE_TEMPLATE/SECURITY.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -name: 🛡 Security -about: Information on reporting security vulnerability ---- - -# Security Policy - -## Supported Versions - -As of November 2019 (and until this document is updated), only the v3.0.0-beta tags of Strapi are supported for updates. Any previous versions are currently not supported and users are advised to use them "at their own risk". - -## Reporting a Vulnerability - -Please report (suspected) security vulnerabilities to -**[security@strapi.io](mailto:security@strapi.io)** or via the [Strapi Slack](https://slack.strapi.io). - -When reporting a (suspected) security vulnerability via slack please reach out to any of the following Strapi employees directly: - -- `@aureliengeorget` -- `@alexandre` -- `@lauriejim` -- `@soupette` - -You will receive a response from us within 72 hours. If the issue is confirmed, -we will release a patch as soon as possible depending on complexity -but historically within a few days. 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/package.json b/packages/strapi-admin/package.json index 379433f1e9..f136a2ec27 100644 --- a/packages/strapi-admin/package.json +++ b/packages/strapi-admin/package.json @@ -47,7 +47,6 @@ "is-wsl": "^2.0.0", "mini-css-extract-plugin": "^0.6.0", "moment": "^2.24.0", - "open-browser-webpack-plugin": "^0.0.5", "prop-types": "^15.7.2", "react": "^16.9.0", "react-copy-to-clipboard": "^5.0.1", diff --git a/packages/strapi-admin/webpack.config.dev.js b/packages/strapi-admin/webpack.config.dev.js index 2a942b7719..1b4ff8ff81 100644 --- a/packages/strapi-admin/webpack.config.dev.js +++ b/packages/strapi-admin/webpack.config.dev.js @@ -27,6 +27,8 @@ module.exports = () => { historyApiFallback: { index: '/admin/', }, + open: true, + openPage: '/admin', }, }; }; 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-plugin-content-type-builder/admin/src/assets/icons/attributes/icon_uuid.png b/packages/strapi-plugin-content-type-builder/admin/src/assets/icons/attributes/icon_uuid.png new file mode 100644 index 0000000000..127115b3d7 Binary files /dev/null and b/packages/strapi-plugin-content-type-builder/admin/src/assets/icons/attributes/icon_uuid.png differ diff --git a/packages/strapi-plugin-content-type-builder/admin/src/translations/en.json b/packages/strapi-plugin-content-type-builder/admin/src/translations/en.json index 2153174b73..97e102631a 100644 --- a/packages/strapi-plugin-content-type-builder/admin/src/translations/en.json +++ b/packages/strapi-plugin-content-type-builder/admin/src/translations/en.json @@ -18,6 +18,7 @@ "attribute.richtext": "Rich text", "attribute.string": "String", "attribute.text": "Text", + "attribute.uuid": "Uuid", "button.attributes.add": "Add New Field", "button.attributes.add.another": "Add Another Field", "button.contentType.add": "Add a Content Type", @@ -169,8 +170,10 @@ "popUpForm.attributes.richtext.name": "Rich text", "popUpForm.attributes.string.description": "Titles, names, paragraphs, list of names", "popUpForm.attributes.string.name": "String", - "popUpForm.attributes.text.description": "Descriptions, text paragraphs, articles ", + "popUpForm.attributes.text.description": "Descriptions, text paragraphs, articles", "popUpForm.attributes.text.name": "Text", + "popUpForm.attributes.uuid.description": "Unique identifier", + "popUpForm.attributes.uuid.name": "Uuid", "popUpForm.choose.attributes.header.title": "Add New Field", "popUpForm.choose.attributes.header.subtitle.model": "Select a field for your content type", "popUpForm.choose.attributes.header.subtitle.group": "Select a field for your group", diff --git a/packages/strapi-plugin-content-type-builder/admin/src/utils/attributeIcons.js b/packages/strapi-plugin-content-type-builder/admin/src/utils/attributeIcons.js index 7c93fd50df..b8038144b8 100644 --- a/packages/strapi-plugin-content-type-builder/admin/src/utils/attributeIcons.js +++ b/packages/strapi-plugin-content-type-builder/admin/src/utils/attributeIcons.js @@ -11,6 +11,7 @@ import relation from '../assets/icons/attributes/icon_relation.png'; import richtext from '../assets/icons/attributes/icon_text.png'; import string from '../assets/icons/attributes/icon_string.png'; import text from '../assets/icons/attributes/icon_text.png'; +import uuid from '../assets/icons/attributes/icon_uuid.png'; const attributeIcons = { boolean, @@ -26,6 +27,7 @@ const attributeIcons = { richtext, string, text, + uuid, }; export default attributeIcons; 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-graphql/services/Resolvers.js b/packages/strapi-plugin-graphql/services/Resolvers.js index b2e9af2d26..340e835f4f 100644 --- a/packages/strapi-plugin-graphql/services/Resolvers.js +++ b/packages/strapi-plugin-graphql/services/Resolvers.js @@ -59,7 +59,9 @@ const buildAssocResolvers = (model, name, { plugin }) => { const { primaryKey, associations = [] } = model; - return associations.reduce((resolver, association) => { + return associations + .filter(association => model.attributes[association.alias].private !== true) + .reduce((resolver, association) => { switch (association.nature) { case 'oneToManyMorph': { resolver[association.alias] = async obj => { 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/Providers.js b/packages/strapi-plugin-users-permissions/services/Providers.js index 6cf9b6b57b..0d0b821739 100644 --- a/packages/strapi-plugin-users-permissions/services/Providers.js +++ b/packages/strapi-plugin-users-permissions/services/Providers.js @@ -91,6 +91,7 @@ exports.connect = (provider, query) => { const params = _.assign(profile, { provider: provider, role: defaultRole.id, + confirmed: true, }); const createdUser = await strapi 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. diff --git a/yarn.lock b/yarn.lock index 1f2161fb93..bf954807f0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12930,18 +12930,6 @@ only@~0.0.2: resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" integrity sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q= -open-browser-webpack-plugin@^0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/open-browser-webpack-plugin/-/open-browser-webpack-plugin-0.0.5.tgz#5e6dc6f8b8797331e212985de218572d84c0521f" - integrity sha1-Xm3G+Lh5czHiEphd4hhXLYTAUh8= - dependencies: - open "0.0.5" - -open@0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/open/-/open-0.0.5.tgz#42c3e18ec95466b6bf0dc42f3a2945c3f0cad8fc" - integrity sha1-QsPhjslUZra/DcQvOilFw/DK2Pw= - opencollective-postinstall@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz#5657f1bede69b6e33a45939b061eb53d3c6c3a89"