Merge branch 'master' into patch-1

This commit is contained in:
Jim LAURIE 2019-11-19 15:11:49 +01:00 committed by GitHub
commit 4b921d8778
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 303 additions and 89 deletions

View File

@ -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',
],

View File

@ -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 });
},
};
```

View File

@ -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 });
}

View File

@ -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`

View File

@ -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();
},
};
```

View File

@ -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

View File

@ -1,12 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import { LoadingIndicatorPage } from 'strapi-helper-plugin';
import { App } from '../../App';
describe('<App />', () => {
it('should render the <AppLoader />', () => {
const renderedComponent = shallow(<App getDataSucceeded={jest.fn()} />);
expect(renderedComponent.find(LoadingIndicatorPage)).toHaveLength(1);
});
});

View File

@ -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();

View File

@ -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',

View File

@ -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 }) => <div>Your input type: <b>{type}</b> does not exist</div>;
const DefaultInputError = ({ type }) => (
<div>
Your input type: <b>{type}</b> does not exist
</div>
);
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 <Input {...props} value={inputValue} />;
@ -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,

View File

@ -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

View File

@ -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;

View File

@ -19,7 +19,7 @@ export const cleanData = (retrievedData, ctLayout, groupLayouts) => {
switch (attrType) {
case 'json':
cleanedData = value;
cleanedData = JSON.parse(value);
break;
case 'date':
cleanedData =

View File

@ -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;
}

View File

@ -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({});
},
},
};

View File

@ -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",

View File

@ -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);
}
},
};

View File

@ -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",

View File

@ -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.