diff --git a/examples/getstarted/api/menu/models/Menu.settings.json b/examples/getstarted/api/menu/models/Menu.settings.json index b4300ad8a9..0b6af898f0 100755 --- a/examples/getstarted/api/menu/models/Menu.settings.json +++ b/examples/getstarted/api/menu/models/Menu.settings.json @@ -17,6 +17,10 @@ "restaurant": { "model": "restaurant", "via": "menu" + }, + "menusections": { + "collection": "menusection", + "via": "menu" } } } \ No newline at end of file diff --git a/examples/getstarted/api/menusection/models/Menusection.settings.json b/examples/getstarted/api/menusection/models/Menusection.settings.json index 07645c83b9..11bbb14b92 100755 --- a/examples/getstarted/api/menusection/models/Menusection.settings.json +++ b/examples/getstarted/api/menusection/models/Menusection.settings.json @@ -25,7 +25,8 @@ "repeatable": true }, "menu": { - "model": "menu" + "model": "menu", + "via": "menusections" } } } \ No newline at end of file diff --git a/packages/strapi-plugin-graphql/services/Mutation.js b/packages/strapi-plugin-graphql/services/Mutation.js index 411e285a05..3b1f590cf0 100644 --- a/packages/strapi-plugin-graphql/services/Mutation.js +++ b/packages/strapi-plugin-graphql/services/Mutation.js @@ -174,7 +174,8 @@ module.exports = { ) ); - return async (obj, options, { context }) => { + return async (obj, options, graphqlCtx) => { + const { context } = graphqlCtx; // Hack to be able to handle permissions for each query. const ctx = Object.assign(_.clone(context), { request: Object.assign(_.clone(context.request), { @@ -234,7 +235,7 @@ module.exports = { : body; } - return resolver.call(null, obj, options, context); + return resolver.call(null, obj, options, graphqlCtx); } // Resolver can be a promise. diff --git a/packages/strapi-plugin-graphql/services/Schema.js b/packages/strapi-plugin-graphql/services/Schema.js index 7ce4304a08..8accdef12c 100644 --- a/packages/strapi-plugin-graphql/services/Schema.js +++ b/packages/strapi-plugin-graphql/services/Schema.js @@ -202,56 +202,63 @@ const schemaBuilder = { // Transform object to only contain function. Object.keys(resolvers).reduce((acc, type) => { - return Object.keys(acc[type]).reduce((acc, resolver) => { + return Object.keys(acc[type]).reduce((acc, resolverName) => { + const resolverObj = acc[type][resolverName]; // Disabled this query. - if (acc[type][resolver] === false) { - delete acc[type][resolver]; + if (resolverObj === false) { + delete acc[type][resolverName]; return acc; } - if (!_.isFunction(acc[type][resolver])) { - acc[type][resolver] = acc[type][resolver].resolver; + if (_.isFunction(resolverObj)) { + return acc; } - if ( - _.isString(acc[type][resolver]) || - _.isPlainObject(acc[type][resolver]) - ) { - const { plugin = '' } = _.isPlainObject(acc[type][resolver]) - ? acc[type][resolver] - : {}; + let plugin; + if (_.has(resolverObj, ['plugin'])) { + plugin = resolverObj.plugin; + } else if (_.has(resolverObj, ['resolver', 'plugin'])) { + plugin = resolverObj.resolver.plugin; + } - switch (type) { - case 'Mutation': { - let name, action; - if (_.isString(acc[type][resolver])) { - [name, action] = acc[type][resolver].split('.'); - } else if ( - _.isPlainObject(acc[type][resolver]) && - _.isString(acc[type][resolver].handler) - ) { - [name, action] = acc[type][resolver].handler.split('.'); - } - - acc[type][resolver] = Mutation.composeMutationResolver({ - _schema: strapi.plugins.graphql.config._schema.graphql, - plugin, - name: _.toLower(name), - action, - }); - break; + switch (type) { + case 'Mutation': { + let name, action; + if ( + _.has(resolverObj, ['resolver']) && + _.isString(resolverObj.resolver) + ) { + [name, action] = resolverObj.resolver.split('.'); + } else if ( + _.has(resolverObj, ['resolver', 'handler']) && + _.isString(resolverObj.handler) + ) { + [name, action] = resolverObj.resolver.handler.split('.'); + } else { + name = null; + action = resolverName; } - case 'Query': - default: - acc[type][resolver] = Query.composeQueryResolver({ - _schema: strapi.plugins.graphql.config._schema.graphql, - plugin, - name: resolver, - isSingular: 'force', // Avoid singular/pluralize and force query name. - }); - break; + + const mutationResolver = Mutation.composeMutationResolver({ + _schema: strapi.plugins.graphql.config._schema.graphql, + plugin, + name: _.toLower(name), + action, + }); + + acc[type][resolverName] = mutationResolver; + break; } + case 'Query': + default: + acc[type][resolverName] = Query.composeQueryResolver({ + _schema: strapi.plugins.graphql.config._schema.graphql, + plugin, + name: resolverName, + isSingular: 'force', // Avoid singular/pluralize and force query name. + }); + break; } return acc; diff --git a/packages/strapi-plugin-upload/package.json b/packages/strapi-plugin-upload/package.json index e84501e8d3..43e230923f 100644 --- a/packages/strapi-plugin-upload/package.json +++ b/packages/strapi-plugin-upload/package.json @@ -46,5 +46,6 @@ "npm": ">=6.0.0" }, "license": "MIT", - "gitHead": "c85658a19b8fef0f3164c19693a45db305dc07a9" + "gitHead": "c85658a19b8fef0f3164c19693a45db305dc07a9", + "devDependencies": {} } diff --git a/packages/strapi-plugin-upload/test/graphqlUpload.test.e2e.js b/packages/strapi-plugin-upload/test/graphqlUpload.test.e2e.js new file mode 100644 index 0000000000..0c3fbe3d14 --- /dev/null +++ b/packages/strapi-plugin-upload/test/graphqlUpload.test.e2e.js @@ -0,0 +1,131 @@ +'use strict'; + +const fs = require('fs'); + +const { registerAndLogin } = require('../../../test/helpers/auth'); +const { createAuthRequest } = require('../../../test/helpers/request'); + +let rq; + +const defaultProviderConfig = { + provider: 'local', + name: 'Local server', + enabled: true, + sizeLimit: 1000000, +}; + +const resetProviderConfigToDefault = () => { + return setConfigOptions(defaultProviderConfig); +}; + +const setConfigOptions = assign => { + return rq.put('/upload/settings/development', { + body: { + ...defaultProviderConfig, + ...assign, + }, + }); +}; + +describe('Upload plugin end to end tests', () => { + beforeAll(async () => { + const token = await registerAndLogin(); + rq = createAuthRequest(token); + }, 60000); + + afterEach(async () => { + await resetProviderConfigToDefault(); + }); + + test('Upload a single file', async () => { + const req = rq.post('/graphql'); + const form = req.form(); + form.append( + 'operations', + JSON.stringify({ + query: /* GraphQL */ ` + mutation uploadFiles($file: Upload!) { + upload(file: $file) { + id + name + mime + url + } + } + `, + variables: { + file: null, + }, + }) + ); + + form.append( + 'map', + JSON.stringify({ + 0: ['variables.file'], + }) + ); + + form.append('0', fs.createReadStream(__dirname + '/rec.jpg')); + + const res = await req; + + expect(res.statusCode).toBe(200); + expect(res.body).toMatchObject({ + data: { + upload: { + id: expect.anything(), + name: 'rec.jpg', + }, + }, + }); + }); + + test('Upload multiple files', async () => { + const req = rq.post('/graphql'); + const form = req.form(); + form.append( + 'operations', + JSON.stringify({ + query: /* GraphQL */ ` + mutation uploadFiles($files: [Upload]!) { + multipleUpload(files: $files) { + id + name + mime + url + } + } + `, + variables: { + files: [null, null], + }, + }) + ); + + form.append( + 'map', + JSON.stringify({ + 0: ['variables.files.0'], + 1: ['variables.files.1'], + }) + ); + + form.append('0', fs.createReadStream(__dirname + '/rec.jpg')); + form.append('1', fs.createReadStream(__dirname + '/rec.jpg')); + + const res = await req; + + expect(res.statusCode).toBe(200); + expect(res.body).toEqual({ + data: { + multipleUpload: expect.arrayContaining([ + expect.objectContaining({ + id: expect.anything(), + name: 'rec.jpg', + }), + ]), + }, + }); + }); +}); diff --git a/packages/strapi-plugin-upload/test/upload.test.e2e.js b/packages/strapi-plugin-upload/test/upload.test.e2e.js index 579c180996..e38e327cfb 100644 --- a/packages/strapi-plugin-upload/test/upload.test.e2e.js +++ b/packages/strapi-plugin-upload/test/upload.test.e2e.js @@ -4,19 +4,8 @@ const fs = require('fs'); // Helpers. const { registerAndLogin } = require('../../../test/helpers/auth'); -// const createModelsUtils = require('../../../test/helpers/models'); -// const form = require('../../../test/helpers/generators'); const { createAuthRequest } = require('../../../test/helpers/request'); -// const cleanDate = entry => { -// delete entry.updatedAt; -// delete entry.createdAt; -// delete entry.created_at; -// delete entry.updated_at; -// }; - -// let data; -// let modelsUtils; let rq; const defaultProviderConfig = { @@ -43,28 +32,6 @@ describe('Upload plugin end to end tests', () => { beforeAll(async () => { const token = await registerAndLogin(); rq = createAuthRequest(token); - - // modelsUtils = createModelsUtils({ rq }); - - // await modelsUtils.createModels([ - // form.article, - // form.tag, - // form.category, - // form.reference, - // form.product, - // form.articlewithtag, - // ]); - }, 60000); - - afterAll(() => { - // modelsUtils.deleteModels([ - // 'article', - // 'tag', - // 'category', - // 'reference', - // 'product', - // 'articlewithtag', - // ]), }, 60000); afterEach(async () => { diff --git a/packages/strapi-plugin-users-permissions/config/schema.graphql b/packages/strapi-plugin-users-permissions/config/schema.graphql index e1593e1f9e..b41b6df2d1 100644 --- a/packages/strapi-plugin-users-permissions/config/schema.graphql +++ b/packages/strapi-plugin-users-permissions/config/schema.graphql @@ -2,7 +2,7 @@ const _ = require('lodash'); module.exports = { type: { - UsersPermissionsPermission: false // Make this type NOT queriable. + UsersPermissionsPermission: false, // Make this type NOT queriable. }, definition: ` type UsersPermissionsMe { @@ -30,105 +30,136 @@ module.exports = { resolverOf: 'User.me', resolver: { plugin: 'users-permissions', - handler: 'User.me' - } + handler: 'User.me', + }, }, role: { + plugin: 'users-permissions', resolverOf: 'UsersPermissions.getRole', - resolver: async (obj, options, { context }) => { - await strapi.plugins['users-permissions'].controllers.userspermissions.getRole(context); + resolver: async (obj, options, { context }) => { + await strapi.plugins[ + 'users-permissions' + ].controllers.userspermissions.getRole(context); return context.body.role; - } + }, }, roles: { description: `Retrieve all the existing roles. You can't apply filters on this query.`, + plugin: 'users-permissions', resolverOf: 'UsersPermissions.getRoles', // Apply the `getRoles` permissions on the resolver. - resolver: async (obj, options, { context }) => { - await strapi.plugins['users-permissions'].controllers.userspermissions.getRoles(context); + resolver: async (obj, options, { context }) => { + await strapi.plugins[ + 'users-permissions' + ].controllers.userspermissions.getRoles(context); return context.body.roles; - } - } + }, + }, }, Mutation: { createRole: { description: 'Create a new role', + plugin: 'users-permissions', resolverOf: 'UsersPermissions.createRole', - resolver: async (obj, options, { context }) => { - await strapi.plugins['users-permissions'].controllers.userspermissions.createRole(context); + resolver: async (obj, options, { context }) => { + await strapi.plugins[ + 'users-permissions' + ].controllers.userspermissions.createRole(context); return { ok: true }; - } + }, }, updateRole: { description: 'Update an existing role', + plugin: 'users-permissions', resolverOf: 'UsersPermissions.updateRole', - resolver: async (obj, options, { context }) => { - await strapi.plugins['users-permissions'].controllers.userspermissions.updateRole(context.params, context.body); + resolver: async (obj, options, { context }) => { + await strapi.plugins[ + 'users-permissions' + ].controllers.userspermissions.updateRole( + context.params, + context.body + ); return { ok: true }; - } + }, }, deleteRole: { description: 'Delete an existing role', + plugin: 'users-permissions', resolverOf: 'UsersPermissions.deleteRole', - resolver: async (obj, options, { context }) => { - await strapi.plugins['users-permissions'].controllers.userspermissions.deleteRole(context); + resolver: async (obj, options, { context }) => { + await strapi.plugins[ + 'users-permissions' + ].controllers.userspermissions.deleteRole(context); return { ok: true }; - } + }, }, createUser: { description: 'Create a new user', + plugin: 'users-permissions', resolverOf: 'User.create', resolver: async (obj, options, { context }) => { - context.params = _.toPlainObject(options.input.where); + context.params = _.toPlainObject(options.input.where); context.request.body = _.toPlainObject(options.input.data); - await strapi.plugins['users-permissions'].controllers.user.create(context); + await strapi.plugins['users-permissions'].controllers.user.create( + context + ); return { - user: context.body.toJSON ? context.body.toJSON() : context.body + user: context.body.toJSON ? context.body.toJSON() : context.body, }; - } + }, }, updateUser: { description: 'Update an existing user', + plugin: 'users-permissions', resolverOf: 'User.update', resolver: async (obj, options, { context }) => { - context.params = _.toPlainObject(options.input.where); + context.params = _.toPlainObject(options.input.where); context.request.body = _.toPlainObject(options.input.data); - await strapi.plugins['users-permissions'].controllers.user.update(context); + await strapi.plugins['users-permissions'].controllers.user.update( + context + ); - return { - user: context.body.toJSON ? context.body.toJSON() : context.body + return { + user: context.body.toJSON ? context.body.toJSON() : context.body, }; - } + }, }, deleteUser: { description: 'Delete an existing user', + plugin: 'users-permissions', resolverOf: 'User.destroy', resolver: async (obj, options, { context }) => { // Set parameters to context. - context.params = _.toPlainObject(options.input.where); + context.params = _.toPlainObject(options.input.where); context.request.body = _.toPlainObject(options.input.data); - // Retrieve user to be able to return it because + // Retrieve user to be able to return it because // Bookshelf doesn't return the row once deleted. - await strapi.plugins['users-permissions'].controllers.user.findOne(context); + await strapi.plugins['users-permissions'].controllers.user.findOne( + context + ); // Assign result to user. - const user = context.body.toJSON ? context.body.toJSON() : context.body; + const user = context.body.toJSON + ? context.body.toJSON() + : context.body; // Run destroy query. - await strapi.plugins['users-permissions'].controllers.user.destroy(context); + await strapi.plugins['users-permissions'].controllers.user.destroy( + context + ); return { - user + user, }; - } - } - } - } + }, + }, + }, + }, }; diff --git a/packages/strapi-plugin-users-permissions/controllers/User.js b/packages/strapi-plugin-users-permissions/controllers/User.js index 2a0451fc93..0307887aed 100644 --- a/packages/strapi-plugin-users-permissions/controllers/User.js +++ b/packages/strapi-plugin-users-permissions/controllers/User.js @@ -170,27 +170,37 @@ module.exports = { const { id } = ctx.params; const { email, username, password } = ctx.request.body; - if (!email) return ctx.badRequest('missing.email'); - if (!username) return ctx.badRequest('missing.username'); - if (!password) return ctx.badRequest('missing.password'); - - const userWithSameUsername = await strapi - .query('user', 'users-permissions') - .findOne({ username }); - - if (userWithSameUsername && userWithSameUsername.id != id) { - return ctx.badRequest( - null, - ctx.request.admin - ? adminError({ - message: 'Auth.form.error.username.taken', - field: ['username'], - }) - : 'username.alreadyTaken.' - ); + if (_.has(ctx.request.body, 'email') && !email) { + return ctx.badRequest('email.notNull'); } - if (advancedConfigs.unique_email) { + if (_.has(ctx.request.body, 'username') && !username) { + return ctx.badRequest('username.notNull'); + } + + if (_.has(ctx.request.body, 'password') && !password) { + return ctx.badRequest('password.notNull'); + } + + if (_.has(ctx.request.body, 'username')) { + const userWithSameUsername = await strapi + .query('user', 'users-permissions') + .findOne({ username }); + + if (userWithSameUsername && userWithSameUsername.id != id) { + return ctx.badRequest( + null, + ctx.request.admin + ? adminError({ + message: 'Auth.form.error.username.taken', + field: ['username'], + }) + : 'username.alreadyTaken.' + ); + } + } + + if (_.has(ctx.request.body, 'email') && advancedConfigs.unique_email) { const userWithSameEmail = await strapi .query('user', 'users-permissions') .findOne({ email }); @@ -216,7 +226,7 @@ module.exports = { ...ctx.request.body, }; - if (password === user.password) { + if (_.has(ctx.request.body, 'password') && password === user.password) { delete updateData.password; } diff --git a/packages/strapi-plugin-users-permissions/test/graphql.test.e2e.js b/packages/strapi-plugin-users-permissions/test/graphql.test.e2e.js new file mode 100644 index 0000000000..b142803fe3 --- /dev/null +++ b/packages/strapi-plugin-users-permissions/test/graphql.test.e2e.js @@ -0,0 +1,241 @@ +// Helpers. +const { registerAndLogin } = require('../../../test/helpers/auth'); + +const { + createAuthRequest, + createRequest, +} = require('../../../test/helpers/request'); + +let authReq; +const data = {}; + +describe('Test Graphql user service', () => { + beforeAll(async () => { + const token = await registerAndLogin(); + authReq = createAuthRequest(token); + }, 60000); + + describe('Check createUser authorizations', () => { + test('createUser is forbidden to public', async () => { + const rq = createRequest(); + const res = await rq({ + url: '/graphql', + method: 'POST', + body: { + query: /* GraphQL */ ` + mutation { + createUser( + input: { + data: { username: "test", email: "test", password: "test" } + } + ) { + user { + id + username + } + } + } + `, + }, + }); + + expect(res.statusCode).toBe(200); + expect(res.body).toMatchObject({ + data: { + createUser: null, + }, + errors: [ + { + message: 'Forbidden', + }, + ], + }); + }); + + test('createUser is authorized for admins', async () => { + const res = await authReq({ + url: '/graphql', + method: 'POST', + body: { + query: /* GraphQL */ ` + mutation { + createUser( + input: { + data: { + username: "test" + email: "test@strapi.io" + password: "test" + } + } + ) { + user { + id + username + } + } + } + `, + }, + }); + + expect(res.statusCode).toBe(201); + expect(res.body).toMatchObject({ + data: { + createUser: { + user: { + id: expect.anything(), + username: 'test', + }, + }, + }, + }); + + data.user = res.body.data.createUser.user; + }); + }); + + describe('Check updateUser authorizations', () => { + test('updateUser is forbidden to public', async () => { + const rq = createRequest(); + const res = await rq({ + url: '/graphql', + method: 'POST', + body: { + query: /* GraphQL */ ` + mutation { + updateUser( + input: { + where: { id: 1 } + data: { username: "test", email: "test", password: "test" } + } + ) { + user { + id + username + } + } + } + `, + }, + }); + + expect(res.statusCode).toBe(200); + expect(res.body).toMatchObject({ + data: { + updateUser: null, + }, + errors: [ + { + message: 'Forbidden', + }, + ], + }); + }); + + test('updateUser is authorized for admins', async () => { + const res = await authReq({ + url: '/graphql', + method: 'POST', + body: { + query: /* GraphQL */ ` + mutation updateUser($id: ID!) { + updateUser( + input: { where: { id: $id }, data: { username: "newUsername" } } + ) { + user { + id + username + } + } + } + `, + variables: { + id: data.user.id, + }, + }, + }); + + expect(res.statusCode).toBe(200); + expect(res.body).toMatchObject({ + data: { + updateUser: { + user: { + id: expect.anything(), + username: 'newUsername', + }, + }, + }, + }); + + data.user = res.body.data.updateUser.user; + }); + }); + + describe('Check deleteUser authorizations', () => { + test('deleteUser is forbidden to public', async () => { + const rq = createRequest(); + const res = await rq({ + url: '/graphql', + method: 'POST', + body: { + query: /* GraphQL */ ` + mutation deleteUser($id: ID!) { + deleteUser(input: { where: { id: $id } }) { + user { + id + username + } + } + } + `, + variables: { + id: data.user.id, + }, + }, + }); + + expect(res.statusCode).toBe(200); + expect(res.body).toMatchObject({ + data: { + deleteUser: null, + }, + errors: [ + { + message: 'Forbidden', + }, + ], + }); + }); + + test('deleteUser is authorized for admins', async () => { + const res = await authReq({ + url: '/graphql', + method: 'POST', + body: { + query: /* GraphQL */ ` + mutation deleteUser($id: ID!) { + deleteUser(input: { where: { id: $id } }) { + user { + id + username + } + } + } + `, + variables: { + id: data.user.id, + }, + }, + }); + + expect(res.statusCode).toBe(200); + expect(res.body).toMatchObject({ + data: { + deleteUser: { + user: data.user, + }, + }, + }); + }); + }); +});