Merge branch 'releases/v4' into v4/graphql-schema-generation-refactor

This commit is contained in:
Convly 2021-09-10 15:55:35 +02:00
commit ee5e7a026f
39 changed files with 711 additions and 624 deletions

View File

@ -45,15 +45,6 @@ const SettingsPage = lazy(() =>
import(/* webpackChunkName: "Admin_settingsPage" */ '../SettingsPage')
);
const CTB = lazy(() =>
import(
/* webpackChunkName: "content-type-builder" */ '@strapi/plugin-content-type-builder/admin/src/pages/App'
)
);
const Upload = lazy(() =>
import(/* webpackChunkName: "upload" */ '@strapi/plugin-upload/admin/src/pages/App')
);
// Simple hook easier for testing
const useTrackUsage = () => {
const { trackUsage } = useTracking();
@ -100,8 +91,6 @@ const Admin = () => {
<Route path="/me" component={ProfilePage} exact />
<Route path="/content-manager" component={CM} />
<Route path="/plugins/content-type-builder" component={CTB} />
<Route path="/plugins/upload" component={Upload} />
{routes}
<Route path="/settings/:settingId" component={SettingsPage} />
<Route path="/settings" component={SettingsPage} exact />

View File

@ -9,14 +9,14 @@ import emailPlugin from '../../../email/admin/src';
import uploadPlugin from '../../../upload/admin/src';
const plugins = {
'@strapi/plugin-content-type-builder': ctbPlugin,
'@strapi/plugin-documentation': documentationPlugin,
'@strapi/plugin-i18n': i18nPlugin,
'@strapi/plugin-email': emailPlugin,
'@strapi/plugin-upload': uploadPlugin,
'@strapi/plugin-graphql': graphqlPlugin,
'@strapi/plugin-sentry': sentryPlugin,
'@strapi/plugin-users-permissions': usersPermissionsPlugin,
'content-type-builder': ctbPlugin,
documentation: documentationPlugin,
i18n: i18nPlugin,
email: emailPlugin,
upload: uploadPlugin,
graphql: graphqlPlugin,
sentry: sentryPlugin,
'users-permissions': usersPermissionsPlugin,
};
export default plugins;

View File

@ -6,16 +6,19 @@ module.exports = {
method: 'GET',
path: '/providers',
handler: 'authentication.getProviders',
config: { auth: false },
},
{
method: 'GET',
path: '/connect/:provider',
handler: 'authentication.providerLogin',
config: { auth: false },
},
{
method: 'POST',
path: '/connect/:provider',
handler: 'authentication.providerLogin',
config: { auth: false },
},
{
method: 'GET',

View File

@ -1,5 +1,5 @@
'use strict';
/* eslint-disable no-useless-escape */
const path = require('path');
const _ = require('lodash');
const fs = require('fs-extra');
@ -34,9 +34,9 @@ function getCustomWebpackConfig(dir, config) {
return webpackConfig;
}
async function build({ dir, env, options, optimize }) {
async function build({ plugins, dir, env, options, optimize }) {
// Create the cache dir containing the front-end files.
await createCacheDir(dir);
await createCacheDir({ dir, plugins });
const cacheDir = path.resolve(dir, '.cache');
const entry = path.resolve(cacheDir, 'admin', 'src');
@ -48,7 +48,18 @@ async function build({ dir, env, options, optimize }) {
ceRoot: path.resolve(cacheDir, 'admin', 'src'),
};
const config = getCustomWebpackConfig(dir, { entry, dest, env, options, optimize, roots });
const pluginsPath = Object.keys(plugins).map(pluginName => plugins[pluginName].pathToPlugin);
const config = getCustomWebpackConfig(dir, {
entry,
pluginsPath,
cacheDir,
dest,
env,
options,
optimize,
roots,
});
const compiler = webpack(config);
@ -78,34 +89,28 @@ async function build({ dir, env, options, optimize }) {
});
}
async function createPluginsJs(plugins, localPlugins, dest) {
const createPluginsArray = plugins =>
plugins.map(name => {
const shortName = _.camelCase(name.replace(/^@strapi\/plugin-/i, ''));
return { name, shortName };
});
const appPluginsArray = createPluginsArray(plugins);
const localPluginsArray = createPluginsArray(localPlugins);
async function createPluginsJs(plugins, dest) {
const pluginsArray = plugins.map(({ pathToPlugin, name }) => {
const shortName = _.camelCase(name);
return {
name,
pathToPlugin: path.relative(path.resolve(dest, 'admin', 'src'), pathToPlugin),
shortName,
};
});
const content = `
${appPluginsArray
.map(({ name, shortName }) => {
const req = `'../../plugins/${name}/admin/src'`;
${pluginsArray
.map(({ pathToPlugin, shortName }) => {
const req = `'${pathToPlugin}/admin/src'`;
return `import ${shortName} from ${req};`;
})
.join('\n')}
${localPluginsArray
.map(({ name, shortName }) => {
const req = `'../../../plugins/${name}/admin/src'`;
return `import ${shortName} from ${req};`;
})
.join('\n')}
const plugins = {
${[...appPluginsArray, ...localPluginsArray]
${[...pluginsArray]
.map(({ name, shortName }) => {
return ` '${name}': ${shortName},`;
})
@ -126,21 +131,6 @@ async function clean({ dir }) {
fs.removeSync(cacheDir);
}
async function copyPlugin(name, dest) {
const pkgFilePath = getPkgPath(name);
const resolveDepPath = (...args) => path.resolve(pkgFilePath, ...args);
const resolveDest = (...args) => path.resolve(dest, 'plugins', name, ...args);
const copy = (...args) => {
return fs.copy(resolveDepPath(...args), resolveDest(...args));
};
// Copy the entire admin folder
await copy('admin');
await copy('package.json');
}
async function copyAdmin(dest) {
const adminPath = getPkgPath('@strapi/admin');
@ -154,25 +144,16 @@ async function copyAdmin(dest) {
await fs.copy(path.resolve(adminPath, 'package.json'), path.resolve(dest, 'package.json'));
}
async function createCacheDir(dir) {
async function createCacheDir({ dir, plugins }) {
const cacheDir = path.resolve(dir, '.cache');
const pkgJSON = require(path.join(dir, 'package.json'));
const pluginsToCopy = Object.keys(pkgJSON.dependencies).filter(
dep =>
dep.startsWith('@strapi/plugin') &&
fs.existsSync(path.resolve(getPkgPath(dep), 'admin', 'src', 'index.js'))
);
let localPluginsToCopy = [];
if (fs.existsSync(path.join(dir, 'plugins'))) {
localPluginsToCopy = fs
.readdirSync(path.join(dir, 'plugins'))
.filter(plugin =>
fs.existsSync(path.resolve(dir, 'plugins', plugin, 'admin', 'src', 'index.js'))
);
}
const pluginsWithFront = Object.keys(plugins)
.filter(pluginName => {
const pluginInfo = plugins[pluginName];
// TODO: use strapi-admin
return fs.existsSync(path.resolve(pluginInfo.pathToPlugin, 'admin', 'src', 'index.js'));
})
.map(name => ({ name, ...plugins[name] }));
// create .cache dir
await fs.emptyDir(cacheDir);
@ -180,9 +161,6 @@ async function createCacheDir(dir) {
// copy admin core code
await copyAdmin(cacheDir);
// copy plugins code
await Promise.all(pluginsToCopy.map(name => copyPlugin(name, cacheDir)));
// Copy app.js
const customAdminConfigFilePath = path.join(dir, 'admin', 'app.js');
@ -198,27 +176,30 @@ async function createCacheDir(dir) {
}
// create plugins.js with plugins requires
await createPluginsJs(pluginsToCopy, localPluginsToCopy, cacheDir);
await createPluginsJs(pluginsWithFront, cacheDir);
}
async function watchAdmin({ dir, host, port, browser, options }) {
async function watchAdmin({ plugins, dir, host, port, browser, options }) {
// Create the cache dir containing the front-end files.
await createCacheDir(dir);
const cacheDir = path.join(dir, '.cache');
await createCacheDir({ dir, plugins });
const entry = path.join(dir, '.cache', 'admin', 'src');
const entry = path.join(cacheDir, 'admin', 'src');
const dest = path.join(dir, 'build');
const env = 'development';
const cacheDir = path.join(dir, '.cache');
// Roots for the @strapi/babel-plugin-switch-ee-ce
const roots = {
eeRoot: path.resolve(cacheDir, 'ee', 'admin'),
ceRoot: path.resolve(cacheDir, 'admin', 'src'),
};
const pluginsPath = Object.keys(plugins).map(pluginName => plugins[pluginName].pathToPlugin);
const args = {
entry,
cacheDir,
pluginsPath,
dest,
env,
port,

View File

@ -11,6 +11,8 @@ const buildAdmin = async () => {
const args = {
entry,
dest,
cacheDir: __dirname,
pluginsPath: [path.resolve(__dirname, '../../../..')],
env: 'production',
optimize: true,
options: {

View File

@ -31,9 +31,9 @@ module.exports = createPolicyFactory(
);
return (ctx, next) => {
const { userAbility: ability, isAuthenticatedAdmin } = ctx.state;
const { userAbility: ability, isAuthenticated } = ctx.state;
if (!isAuthenticatedAdmin || !ability) {
if (!isAuthenticated || !ability) {
return next();
}

View File

@ -1,7 +1,7 @@
'use strict';
module.exports = (ctx, next) => {
if (!ctx.state.isAuthenticatedAdmin) {
if (!ctx.state.isAuthenticated) {
return ctx.unauthorized();
}

View File

@ -3,56 +3,51 @@
// const permissionsFieldsToPropertiesMigration = require('../migrations/permissions-fields-to-properties');
/**
* Tries to authenticated admin user and calls next.
* @param {KoaContext} ctx
* @param {Middleware} next
* @returns {undefined}
*/
const authMiddleware = async (ctx, next) => {
if (!ctx.request.header.authorization) {
return next();
}
const adminAuthStrategy = {
name: 'admin',
async authenticate(ctx) {
const { authorization } = ctx.request.header;
if (
ctx.request.header.authorization &&
ctx.request.header.authorization.split(' ')[0] === 'Bearer'
) {
const token = ctx.request.header.authorization.split(' ')[1];
if (!authorization) {
return { authenticated: false };
}
const parts = authorization.split(/\s+/);
if (parts[0] !== 'Bearer' || parts.length !== 2) {
return { authenticated: false };
}
const token = parts[1];
const { payload, isValid } = strapi.admin.services.token.decodeJwtToken(token);
if (isValid) {
const admin = await strapi
const user = await strapi
.query('admin::user')
.findOne({ where: { id: payload.id }, populate: ['roles'] });
if (!admin || !(admin.isActive === true)) {
return ctx.unauthorized('Invalid credentials');
if (!user || !(user.isActive === true)) {
return { error: 'Invalid credentials' };
}
// TODO: use simple user & isAuthenticated
const userAbility = await strapi.admin.services.permission.engine.generateUserAbility(user);
ctx.state.admin = admin;
ctx.state.user = admin;
ctx.state.userAbility = await strapi.admin.services.permission.engine.generateUserAbility(
admin
);
ctx.state.userAbility = userAbility;
ctx.state.user = user;
ctx.state.isAuthenticatedAdmin = true;
return next();
return { authenticated: true, credentials: user };
}
}
ctx.unauthorized('Invalid credentials');
return { error: 'Invalid credentials' };
},
// async verify() {},
};
module.exports = () => {
const passportMiddleware = strapi.admin.services.passport.init();
strapi.server.api('admin').use(passportMiddleware);
strapi.server.api('admin').use(authMiddleware);
strapi.container.get('auth').register('admin', adminAuthStrategy);
// FIXME: to implement
// strapi.db.migrations.register(permissionsFieldsToPropertiesMigration);

View File

@ -0,0 +1,63 @@
'use strict';
module.exports = [
{
method: 'GET',
path: '/init',
handler: 'admin.init',
config: { auth: false },
},
{
method: 'GET',
path: '/project-type',
handler: 'admin.getProjectType',
config: { auth: false },
},
{
method: 'GET',
path: '/information',
handler: 'admin.information',
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
},
{
method: 'GET',
path: '/plugins',
handler: 'admin.plugins',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::marketplace.read'] } },
],
},
},
{
method: 'POST',
path: '/plugins/install',
handler: 'admin.installPlugin',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{
name: 'admin::hasPermissions',
options: { actions: ['admin::marketplace.plugins.install'] },
},
],
},
},
{
method: 'DELETE',
path: '/plugins/uninstall/:plugin',
handler: 'admin.uninstallPlugin',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{
name: 'admin::hasPermissions',
options: { actions: ['admin::marketplace.plugins.uninstall'] },
},
],
},
},
];

View File

@ -0,0 +1,46 @@
'use strict';
module.exports = [
{
method: 'POST',
path: '/login',
handler: 'authentication.login',
config: { auth: false },
},
{
method: 'POST',
path: '/renew-token',
handler: 'authentication.renewToken',
config: { auth: false },
},
{
method: 'POST',
path: '/register-admin',
handler: 'authentication.registerAdmin',
config: { auth: false },
},
{
method: 'GET',
path: '/registration-info',
handler: 'authentication.registrationInfo',
config: { auth: false },
},
{
method: 'POST',
path: '/register',
handler: 'authentication.register',
config: { auth: false },
},
{
method: 'POST',
path: '/forgot-password',
handler: 'authentication.forgotPassword',
config: { auth: false },
},
{
method: 'POST',
path: '/reset-password',
handler: 'authentication.resetPassword',
config: { auth: false },
},
];

View File

@ -1,325 +1,10 @@
'use strict';
module.exports = [
{
method: 'GET',
path: '/init',
handler: 'admin.init',
},
{
method: 'GET',
path: '/project-type',
handler: 'admin.getProjectType',
},
{
method: 'GET',
path: '/information',
handler: 'admin.information',
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
},
{
method: 'GET',
path: '/plugins',
handler: 'admin.plugins',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::marketplace.read'] } },
],
},
},
{
method: 'POST',
path: '/plugins/install',
handler: 'admin.installPlugin',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{
name: 'admin::hasPermissions',
options: { actions: ['admin::marketplace.plugins.install'] },
},
],
},
},
{
method: 'DELETE',
path: '/plugins/uninstall/:plugin',
handler: 'admin.uninstallPlugin',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{
name: 'admin::hasPermissions',
options: { actions: ['admin::marketplace.plugins.uninstall'] },
},
],
},
},
{
method: 'POST',
path: '/login',
handler: 'authentication.login',
},
{
method: 'POST',
path: '/renew-token',
handler: 'authentication.renewToken',
},
{
method: 'POST',
path: '/register-admin',
handler: 'authentication.registerAdmin',
},
{
method: 'GET',
path: '/registration-info',
handler: 'authentication.registrationInfo',
},
{
method: 'POST',
path: '/register',
handler: 'authentication.register',
},
{
method: 'POST',
path: '/forgot-password',
handler: 'authentication.forgotPassword',
},
{
method: 'POST',
path: '/reset-password',
handler: 'authentication.resetPassword',
},
{
method: 'GET',
path: '/webhooks',
handler: 'Webhooks.listWebhooks',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::webhooks.read'] } },
],
},
},
{
method: 'POST',
path: '/webhooks',
handler: 'Webhooks.createWebhook',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::webhooks.create'] } },
],
},
},
{
method: 'GET',
path: '/webhooks/:id',
handler: 'Webhooks.getWebhook',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::webhooks.read'] } },
],
},
},
{
method: 'PUT',
path: '/webhooks/:id',
handler: 'Webhooks.updateWebhook',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::webhooks.update'] } },
],
},
},
{
method: 'DELETE',
path: '/webhooks/:id',
handler: 'Webhooks.deleteWebhook',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::webhooks.delete'] } },
],
},
},
{
method: 'POST',
path: '/webhooks/batch-delete',
handler: 'Webhooks.deleteWebhooks',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::webhooks.delete'] } },
],
},
},
{
method: 'POST',
path: '/webhooks/:id/trigger',
handler: 'Webhooks.triggerWebhook',
config: {
policies: [],
},
},
{
method: 'GET',
path: '/users/me',
handler: 'authenticated-user.getMe',
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
},
{
method: 'PUT',
path: '/users/me',
handler: 'authenticated-user.updateMe',
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
},
{
method: 'GET',
path: '/users/me/permissions',
handler: 'authenticated-user.getOwnPermissions',
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
},
{
method: 'POST',
path: '/users',
handler: 'user.create',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::users.create'] } },
],
},
},
{
method: 'GET',
path: '/users',
handler: 'user.find',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::users.read'] } },
],
},
},
{
method: 'GET',
path: '/users/:id',
handler: 'user.findOne',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::users.read'] } },
],
},
},
{
method: 'PUT',
path: '/users/:id',
handler: 'user.update',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::users.update'] } },
],
},
},
{
method: 'DELETE',
path: '/users/:id',
handler: 'user.deleteOne',
config: {
policies: [{ name: 'admin::hasPermissions', options: { actions: ['admin::users.delete'] } }],
},
},
{
method: 'POST',
path: '/users/batch-delete',
handler: 'user.deleteMany',
config: {
policies: [{ name: 'admin::hasPermissions', options: { actions: ['admin::users.delete'] } }],
},
},
{
method: 'GET',
path: '/roles/:id/permissions',
handler: 'role.getPermissions',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::roles.read'] } },
],
},
},
{
method: 'PUT',
path: '/roles/:id/permissions',
handler: 'role.updatePermissions',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::roles.update'] } },
],
},
},
{
method: 'GET',
path: '/roles/:id',
handler: 'role.findOne',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::roles.read'] } },
],
},
},
{
method: 'GET',
path: '/roles',
handler: 'role.findAll',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::roles.read'] } },
],
},
},
{
method: 'PUT',
path: '/roles/:id',
handler: 'role.update',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::roles.update'] } },
],
},
},
{
method: 'GET',
path: '/permissions',
handler: 'permission.getAll',
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
},
{
method: 'POST',
path: '/permissions/check',
handler: 'permission.check',
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
},
];
const admin = require('./admin');
const authentication = require('./authentication');
const permissions = require('./permissions');
const users = require('./users');
const roles = require('./roles');
const webhooks = require('./webhooks');
module.exports = [...admin, ...authentication, ...permissions, ...users, ...roles, ...webhooks];

View File

@ -0,0 +1,20 @@
'use strict';
module.exports = [
{
method: 'GET',
path: '/permissions',
handler: 'permission.getAll',
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
},
{
method: 'POST',
path: '/permissions/check',
handler: 'permission.check',
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
},
];

View File

@ -0,0 +1,67 @@
'use strict';
module.exports = [
{
method: 'POST',
path: '/users/batch-delete',
handler: 'user.deleteMany',
config: {
policies: [{ name: 'admin::hasPermissions', options: { actions: ['admin::users.delete'] } }],
},
},
{
method: 'GET',
path: '/roles/:id/permissions',
handler: 'role.getPermissions',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::roles.read'] } },
],
},
},
{
method: 'PUT',
path: '/roles/:id/permissions',
handler: 'role.updatePermissions',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::roles.update'] } },
],
},
},
{
method: 'GET',
path: '/roles/:id',
handler: 'role.findOne',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::roles.read'] } },
],
},
},
{
method: 'GET',
path: '/roles',
handler: 'role.findAll',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::roles.read'] } },
],
},
},
{
method: 'PUT',
path: '/roles/:id',
handler: 'role.update',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::roles.update'] } },
],
},
},
];

View File

@ -0,0 +1,88 @@
'use strict';
module.exports = [
{
method: 'GET',
path: '/users/me',
handler: 'authenticated-user.getMe',
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
},
{
method: 'PUT',
path: '/users/me',
handler: 'authenticated-user.updateMe',
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
},
{
method: 'GET',
path: '/users/me/permissions',
handler: 'authenticated-user.getOwnPermissions',
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
},
{
method: 'POST',
path: '/users',
handler: 'user.create',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::users.create'] } },
],
},
},
{
method: 'GET',
path: '/users',
handler: 'user.find',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::users.read'] } },
],
},
},
{
method: 'GET',
path: '/users/:id',
handler: 'user.findOne',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::users.read'] } },
],
},
},
{
method: 'PUT',
path: '/users/:id',
handler: 'user.update',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::users.update'] } },
],
},
},
{
method: 'DELETE',
path: '/users/:id',
handler: 'user.deleteOne',
config: {
policies: [{ name: 'admin::hasPermissions', options: { actions: ['admin::users.delete'] } }],
},
},
{
method: 'POST',
path: '/users/batch-delete',
handler: 'user.deleteMany',
config: {
policies: [{ name: 'admin::hasPermissions', options: { actions: ['admin::users.delete'] } }],
},
},
];

View File

@ -0,0 +1,78 @@
'use strict';
module.exports = [
{
method: 'GET',
path: '/webhooks',
handler: 'Webhooks.listWebhooks',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::webhooks.read'] } },
],
},
},
{
method: 'POST',
path: '/webhooks',
handler: 'Webhooks.createWebhook',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::webhooks.create'] } },
],
},
},
{
method: 'GET',
path: '/webhooks/:id',
handler: 'Webhooks.getWebhook',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::webhooks.read'] } },
],
},
},
{
method: 'PUT',
path: '/webhooks/:id',
handler: 'Webhooks.updateWebhook',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::webhooks.update'] } },
],
},
},
{
method: 'DELETE',
path: '/webhooks/:id',
handler: 'Webhooks.deleteWebhook',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::webhooks.delete'] } },
],
},
},
{
method: 'POST',
path: '/webhooks/batch-delete',
handler: 'Webhooks.deleteWebhooks',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::webhooks.delete'] } },
],
},
},
{
method: 'POST',
path: '/webhooks/:id/trigger',
handler: 'Webhooks.triggerWebhook',
config: {
policies: [],
},
},
];

View File

@ -22,6 +22,8 @@ module.exports = () => {
const args = {
entry,
cacheDir: __dirname,
pluginsPath: [path.resolve(__dirname, '../../..')],
dest,
env,
options,

View File

@ -13,6 +13,8 @@ const getClientEnvironment = require('./env');
module.exports = ({
entry,
cacheDir,
pluginsPath,
dest,
env,
optimize,
@ -98,7 +100,7 @@ module.exports = ({
{
test: /\.m?js$/,
// TODO remove when plugins are built separately
exclude: /node_modules\/(?!(@strapi\/plugin-content-type-builder|@strapi\/plugin-upload)\/).*/,
include: [cacheDir, ...pluginsPath],
use: {
loader: require.resolve('babel-loader'),
options: {

View File

@ -19,14 +19,22 @@ const name = pluginPkg.strapi.name;
export default {
register(app) {
app.addReducers(reducers);
app.addCorePluginMenuLink({
app.addMenuLink({
to: `/plugins/${pluginId}`,
icon,
intlLabel: {
id: `${pluginId}.plugin.name`,
defaultMessage: 'Content-Types Builder',
defaultMessage: 'Content Types Builder',
},
permissions: pluginPermissions.main,
Component: async () => {
const component = await import(
/* webpackChunkName: "content-type-builder" */ './pages/App'
);
return component;
},
});
app.registerPlugin({

View File

@ -7,7 +7,9 @@ module.exports = {
method: 'POST',
path: '/',
handler: 'email.send',
config: {},
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
},
{
method: 'POST',

View File

@ -18,7 +18,7 @@ const { createCoreStore, coreStoreModel } = require('./services/core-store');
const createEntityService = require('./services/entity-service');
const entityValidator = require('./services/entity-validator');
const createTelemetry = require('./services/metrics');
const createContentAPI = require('./services/content-api');
const createAuth = require('./services/auth');
const createUpdateNotifier = require('./utils/update-notifier');
const createStartupLogger = require('./utils/startup-logger');
const ee = require('./utils/ee');
@ -53,7 +53,7 @@ class Strapi {
this.container.register('modules', modulesRegistry(this));
this.container.register('plugins', pluginsRegistry(this));
this.container.register('apis', apisRegistry(this));
this.container.register('content-api', createContentAPI(this));
this.container.register('auth', createAuth(this));
this.isLoaded = false;
this.reload = this.reload();

View File

@ -3,20 +3,30 @@ const { green } = require('chalk');
const strapiAdmin = require('@strapi/admin');
const { getConfigUrls } = require('@strapi/utils');
const loadConfiguration = require('../core/app-configuration');
const ee = require('../utils/ee');
const ee = require('../utils/ee');
const addSlash = require('../utils/addSlash');
const strapi = require('../index');
const getEnabledPlugins = require('../core/loaders/plugins/get-enabled-plugins');
/**
* `$ strapi build`
*/
module.exports = async ({ clean, optimization }) => {
const dir = process.cwd();
const config = loadConfiguration(dir);
const { serverUrl, adminPath } = getConfigUrls(config.server, true);
const strapiInstance = strapi({
dir,
autoReload: true,
serveAdminPanel: false,
});
console.log(`Building your admin UI with ${green(config.environment)} configuration ...`);
const plugins = await getEnabledPlugins(strapiInstance);
const env = strapiInstance.config.get('environment');
const { serverUrl, adminPath } = getConfigUrls(strapiInstance.config.get('server'), true);
console.log(`Building your admin UI with ${green(env)} configuration ...`);
if (clean) {
await strapiAdmin.clean({ dir });
@ -27,6 +37,7 @@ module.exports = async ({ clean, optimization }) => {
return strapiAdmin
.build({
dir,
plugins,
// front end build env is always production for now
env: 'production',
optimize: optimization,

View File

@ -1,32 +1,42 @@
'use strict';
const strapiAdmin = require('@strapi/admin');
const { getOr } = require('lodash/fp');
const { getConfigUrls, getAbsoluteServerUrl } = require('@strapi/utils');
const loadConfiguration = require('../core/app-configuration');
const ee = require('../utils/ee');
const addSlash = require('../utils/addSlash');
const strapi = require('../index');
const getEnabledPlugins = require('../core/loaders/plugins/get-enabled-plugins');
module.exports = async function({ browser }) {
const dir = process.cwd();
const config = loadConfiguration(dir);
const strapiInstance = strapi({
dir,
autoReload: true,
serveAdminPanel: false,
});
const { adminPath } = getConfigUrls(config.server, true);
const plugins = await getEnabledPlugins(strapiInstance);
const adminPort = getOr(8000, 'server.admin.port')(config);
const adminHost = getOr('localhost', 'server.admin.host')(config);
const adminWatchIgnoreFiles = getOr([], 'server.admin.watchIgnoreFiles')(config);
const { adminPath } = getConfigUrls(strapiInstance.config.get('server'), true);
const adminPort = strapiInstance.config.get('server.admin.port', 8000);
const adminHost = strapiInstance.config.get('server.admin.host', 'localhost');
const adminWatchIgnoreFiles = strapiInstance.config.get('server.admin.watchIgnoreFiles', []);
const backendURL = getAbsoluteServerUrl(strapiInstance.config, true);
ee({ dir });
strapiAdmin.watchAdmin({
dir,
plugins,
port: adminPort,
host: adminHost,
browser,
options: {
backend: getAbsoluteServerUrl(config, true),
backend: backendURL,
adminPath: addSlash(adminPath),
watchIgnoreFiles: adminWatchIgnoreFiles,
features: ee.isEE ? ee.features.getEnabled() : [],

View File

@ -3,6 +3,7 @@
const { uniq, difference, get, isUndefined, merge } = require('lodash');
const requiredMiddlewares = [
'auth',
'responses',
'router',
'logger',

View File

@ -67,11 +67,13 @@ module.exports = strapi => {
method: 'GET',
path: '/',
handler: serveIndexPage,
config: { auth: false },
},
{
method: 'GET',
path: '/index.html',
handler: serveIndexPage,
config: { auth: false },
},
{
method: 'GET',
@ -80,6 +82,7 @@ module.exports = strapi => {
maxage: maxAge,
defer: true,
}),
config: { auth: false },
},
{
method: 'GET',
@ -88,6 +91,7 @@ module.exports = strapi => {
maxage: maxAge,
defer: true,
}),
config: { auth: false },
},
]);
}
@ -118,6 +122,7 @@ module.exports = strapi => {
serveAdmin,
serveStatic(buildDir, { maxage: maxAge, defer: false, index: 'index.html' }),
],
config: { auth: false },
},
]);
},

View File

@ -0,0 +1,92 @@
'use strict';
const { strict: assert } = require('assert');
const { has, prop } = require('lodash/fp');
class UnauthorizedError extends Error {}
class ForbiddenError extends Error {}
const INVALID_STRATEGY_MSG =
'Invalid auth strategy. Expecting an object with properties {name: string, authenticate: function, verify: function}';
const validStrategy = strategy => {
assert(has('authenticate', strategy), INVALID_STRATEGY_MSG);
assert(typeof strategy.authenticate === 'function', INVALID_STRATEGY_MSG);
if (has('verify', strategy)) {
assert(typeof strategy.verify === 'function', INVALID_STRATEGY_MSG);
}
};
const createAuthentication = () => {
const strategies = {};
return {
errors: {
UnauthorizedError,
ForbiddenError,
},
register(type, strategy) {
validStrategy(strategy);
if (!strategies[type]) {
strategies[type] = [];
}
strategies[type].push(strategy);
return this;
},
async authenticate(ctx, next) {
const { route } = ctx.state;
// use route strategy
const config = prop('config.auth', route);
if (config === false) {
return next();
}
const strategiesToUse = strategies[route.info.type];
for (const strategy of strategiesToUse) {
const result = await strategy.authenticate(ctx);
const { authenticated = false, error = null, credentials } = result || {};
if (error !== null) {
return ctx.unauthorized(error);
}
if (authenticated) {
ctx.state.isAuthenticated = true;
ctx.state.auth = {
strategy,
credentials,
};
return next();
}
}
return ctx.unauthorized('Missing credentials');
},
async verify(auth, config = {}) {
if (config === false) {
return;
}
if (!auth) {
throw new UnauthorizedError();
}
if (typeof auth.strategy.verify === 'function') {
return await auth.strategy.verify(auth, config);
}
return;
},
};
};
module.exports = createAuthentication;

View File

@ -1,71 +0,0 @@
'use strict';
const { strict: assert } = require('assert');
const { has } = require('lodash/fp');
class UnauthorizedError extends Error {}
class ForbiddenError extends Error {}
const INVALID_STRATEGY_MSG =
'Invalid auth strategy. Expecting an object with properties {name: string, authenticate: function, verify: function}';
const validStrategy = strategy => {
assert(has('name', strategy), INVALID_STRATEGY_MSG);
assert(typeof strategy.name === 'string', INVALID_STRATEGY_MSG);
assert(has('authenticate', strategy), INVALID_STRATEGY_MSG);
assert(typeof strategy.authenticate === 'function', INVALID_STRATEGY_MSG);
assert(has('verify', strategy), INVALID_STRATEGY_MSG);
assert(typeof strategy.verify === 'function', INVALID_STRATEGY_MSG);
};
const createAuthentication = () => {
const strategies = [];
return {
register(strategy) {
validStrategy(strategy);
strategies.push(strategy);
return () => {
strategies.splice(strategies.indexOf(strategy), 1);
};
},
async authenticate(ctx, next) {
for (const strategy of strategies) {
const result = await strategy.authenticate(ctx);
const { authenticated = false, credentials } = result || {};
if (authenticated) {
ctx.state.auth = { strategy, credentials };
return next();
}
}
return next();
},
async verify(auth, config = {}) {
if (config.public) {
return undefined;
}
if (!auth) {
throw new UnauthorizedError();
}
return await auth.strategy.verify(auth, config);
},
};
};
module.exports = () => {
return {
auth: createAuthentication(),
errors: {
UnauthorizedError,
ForbiddenError,
},
};
};

View File

@ -5,6 +5,7 @@ const { createAPI } = require('./api');
const createAdminAPI = strapi => {
const opts = {
prefix: '', // '/admin';
type: 'admin',
};
return createAPI(strapi, opts);

View File

@ -5,11 +5,11 @@ const Router = require('@koa/router');
const { createRouteManager } = require('./routing');
const createAPI = (strapi, opts = {}) => {
const { prefix, defaultPolicies } = opts;
const { prefix, type } = opts;
const api = new Router({ prefix });
const routeManager = createRouteManager(strapi, { defaultPolicies });
const routeManager = createRouteManager(strapi, { type });
return {
use(fn) {

View File

@ -1,6 +1,6 @@
'use strict';
const { toLower, castArray, trim } = require('lodash/fp');
const { toLower, castArray, trim, prop } = require('lodash/fp');
const compose = require('koa-compose');
const { resolveMiddlewares } = require('./middleware');
@ -9,24 +9,64 @@ const { resolvePolicies } = require('./policy');
const getMethod = route => trim(toLower(route.method));
const getPath = route => trim(route.path);
const routeInfoMiddleware = route => (ctx, next) => {
const createRouteInfoMiddleware = routeInfo => (ctx, next) => {
const route = {
...routeInfo,
config: routeInfo.config || {},
};
ctx.state.route = route;
return next();
};
const getAuthConfig = prop('config.auth');
const createAuthorizeMiddleware = strapi => async (ctx, next) => {
const { auth, route } = ctx.state;
const authService = strapi.container.get('auth');
try {
await authService.verify(auth, getAuthConfig(route));
return next();
} catch (error) {
const { UnauthorizedError, ForbiddenError } = authService.errors;
if (error instanceof UnauthorizedError) {
return ctx.unauthorized();
}
if (error instanceof ForbiddenError) {
return ctx.forbidden();
}
throw error;
}
};
const createAuthenticateMiddleware = strapi => async (ctx, next) => {
return strapi.container.get('auth').authenticate(ctx, next);
};
module.exports = strapi => {
return (route, { pluginName, router, apiName }) => {
const authenticate = createAuthenticateMiddleware(strapi);
const authorize = createAuthorizeMiddleware(strapi);
return (route, { router }) => {
try {
const method = getMethod(route);
const path = getPath(route);
const middlewares = resolveMiddlewares(route);
const policies = resolvePolicies(route, { pluginName, apiName });
const policies = resolvePolicies(route);
const action = getAction(route, { pluginName, apiName }, strapi);
const action = getAction(route, strapi);
const routeHandler = compose([
routeInfoMiddleware(route),
createRouteInfoMiddleware(route),
authenticate,
authorize,
...policies,
...middlewares,
...castArray(action),
@ -52,7 +92,10 @@ const getController = (name, { pluginName, apiName }, strapi) => {
return strapi.controller(name);
};
const getAction = ({ handler }, { pluginName, apiName }, strapi) => {
const getAction = (route, strapi) => {
const { handler, info = {} } = route;
const { pluginName, apiName } = info;
if (Array.isArray(handler) || typeof handler === 'function') {
return handler;
}

View File

@ -1,50 +1,14 @@
'use strict';
const { prop } = require('lodash/fp');
const { createAPI } = require('./api');
const getAuthConfig = prop('config.auth');
const createAuthPolicy = strapi => async (ctx, next) => {
const { auth, route } = ctx.state;
if (!route) {
return ctx.unauthorized();
}
try {
await strapi.container.get('content-api').auth.verify(auth, getAuthConfig(route));
return next();
} catch (error) {
const { errors } = strapi.container.get('content-api');
if (error instanceof errors.UnauthorizedError) {
return ctx.unauthorized();
}
if (error instanceof errors.ForbiddenError) {
return ctx.forbidden();
}
throw error;
}
};
const createContentAPI = strapi => {
const opts = {
prefix: strapi.config.get('api.prefix', '/api'),
defaultPolicies: [createAuthPolicy(strapi)],
type: 'content-api',
};
const api = createAPI(strapi, opts);
// implement auth providers
api.use((ctx, next) => {
return strapi.container.get('content-api').auth.authenticate(ctx, next);
});
return api;
return createAPI(strapi, opts);
};
module.exports = {

View File

@ -8,13 +8,9 @@ const { createRouteManager } = require('./routing');
const { createAdminAPI } = require('./admin-api');
const { createContentAPI } = require('./content-api');
const healthCheck = async (ctx, next) => {
if (ctx.request.url === '/_health' && ['HEAD', 'GET'].includes(ctx.request.method)) {
ctx.set('strapi', 'You are so French!');
ctx.status = 204;
} else {
await next();
}
const healthCheck = async ctx => {
ctx.set('strapi', 'You are so French!');
ctx.status = 204;
};
/**
@ -49,7 +45,7 @@ const createServer = strapi => {
};
// init health check
app.use(healthCheck);
router.all('/_health', healthCheck);
const state = {
mounted: false,

View File

@ -7,10 +7,14 @@ const { bodyPolicy } = policy;
const getPoliciesConfig = propOr([], 'config.policies');
const resolvePolicies = (route, opts = {}) => {
const resolvePolicies = route => {
const { pluginName, apiName } = route.info || {};
const policiesConfig = getPoliciesConfig(route);
const policies = policiesConfig.map(policyName => policy.get(policyName, opts));
const policies = policiesConfig.map(policyName => {
return policy.get(policyName, { pluginName, apiName });
});
return [...policies, bodyPolicy];
};

View File

@ -69,20 +69,16 @@ const validateRouteConfig = routeConfig => {
};
const createRouteManager = (strapi, opts = {}) => {
const { type } = opts;
const composeEndpoint = createEndpointComposer(strapi);
const createRoute = (route, router) => {
validateRouteConfig(route);
if (opts.defaultPolicies) {
if (has('config.policies', route)) {
route.config.policies.unshift(...opts.defaultPolicies);
} else {
_.set(route, 'config.policies', [...opts.defaultPolicies]);
}
}
_.set(route, 'info.type', type || 'admin');
composeEndpoint(route, { ...route.info, router });
composeEndpoint(route, { router });
};
const addRoutes = (routes, router) => {

View File

@ -24,7 +24,7 @@ export default {
// TODO update doc and guides
app.addComponents({ name: 'media-library', Component: InputModalStepper });
app.addCorePluginMenuLink({
app.addMenuLink({
to: `/plugins/${pluginId}`,
icon,
intlLabel: {
@ -32,6 +32,11 @@ export default {
defaultMessage: 'Media Library',
},
permissions: pluginPermissions.main,
Component: async () => {
const component = await import(/* webpackChunkName: "upload" */ './pages/App');
return component;
},
});
// TODO update guide

View File

@ -32,6 +32,7 @@ module.exports = {
method: 'GET',
path: '/uploads/(.*)',
handler: [range, koaStatic(staticDir, { defer: true, ...localServerConfig })],
config: { auth: false },
},
]);
},

View File

@ -50,8 +50,6 @@ module.exports = (projectDirectory, cliArguments) => {
installDependencies: true,
strapiDependencies: [
'@strapi/strapi',
'@strapi/admin',
'@strapi/utils',
'@strapi/plugin-users-permissions',
'@strapi/plugin-i18n',
],

View File

@ -20,34 +20,34 @@ const authenticate = async ctx => {
const { id } = await getService('jwt').getToken(ctx);
if (id === undefined) {
throw new Error('Invalid token: Token did not contain required fields');
return { error: 'Invalid token: Token did not contain required fields' };
}
// fetch authenticated user
const user = await getService('user').fetchAuthenticatedUser(id);
if (user) {
return {
authenticated: true,
credentials: user,
};
if (!user) {
return { error: 'Invalid credentials' };
}
const advancedSettings = await getAdvancedSettings();
if (advancedSettings.email_confirmation && !user.confirmed) {
return { error: 'Invalid credentials' };
}
if (user.blocked) {
return { error: 'Invalid credentials' };
}
ctx.state.user = user;
return {
authenticated: true,
credentials: user,
};
} catch (err) {
return { authenticated: false };
}
if (!ctx.state.user) {
return { authenticated: false };
}
const advancedSettings = await getAdvancedSettings();
if (advancedSettings.email_confirmation && !ctx.state.user.confirmed) {
return { authenticated: false };
}
if (ctx.state.user.blocked) {
return { authenticated: false };
return { error: 'Invalid credentials' };
}
}
@ -68,7 +68,7 @@ const authenticate = async ctx => {
};
const verify = async (auth, config) => {
const { errors } = strapi.container.get('content-api');
const { errors } = strapi.container.get('auth');
const { credentials: user } = auth;

View File

@ -3,5 +3,5 @@
const authStrategy = require('./auth/strategy');
module.exports = strapi => {
strapi.container.get('content-api').auth.register(authStrategy);
strapi.container.get('auth').register('content-api', authStrategy);
};

View File

@ -20,10 +20,8 @@ const createStrapiInstance = async ({ ensureSuperAdmin = true, logLevel = 'fatal
const options = { dir: TEST_APP_URL };
const instance = strapi(options);
await instance.load();
instance.container.get('content-api').auth.register({
name: 'test-strategy',
instance.container.get('auth').register('content-api', {
name: 'test-auth',
authenticate() {
return { authenticated: true };
},
@ -32,6 +30,8 @@ const createStrapiInstance = async ({ ensureSuperAdmin = true, logLevel = 'fatal
},
});
await instance.load();
instance.log.level = logLevel;
instance.server.mount();