Convly 481550a0d4 Merge branch 'refs/heads/develop' into v5/main
# Conflicts:
#	.github/actions/check-pr-status/package.json
#	docs/docs/docs/01-core/admin/05-features/authentication.md
#	docs/docusaurus.config.js
#	docs/yarn.lock
#	examples/getstarted/package.json
#	examples/kitchensink-ts/package.json
#	examples/kitchensink/package.json
#	examples/plugins/workspace-plugin/package.json
#	lerna.json
#	package.json
#	packages/admin-test-utils/package.json
#	packages/cli/create-strapi-app/package.json
#	packages/cli/create-strapi-starter/package.json
#	packages/cli/create-strapi/package.json
#	packages/core/admin/admin/src/App.tsx
#	packages/core/admin/admin/src/components/AuthenticatedApp.tsx
#	packages/core/admin/admin/src/components/Context.tsx
#	packages/core/admin/admin/src/components/LeftMenu.tsx
#	packages/core/admin/admin/src/components/PrivateRoute.tsx
#	packages/core/admin/admin/src/content-manager/pages/ListSettingsView/index.jsx
#	packages/core/admin/admin/src/content-manager/pages/ListView/components/BulkActions/PublishAction.tsx
#	packages/core/admin/admin/src/content-manager/pages/ListView/components/TableCells/CellContent.tsx
#	packages/core/admin/admin/src/features/Configuration.tsx
#	packages/core/admin/admin/src/index.ts
#	packages/core/admin/admin/src/pages/ProfilePage.tsx
#	packages/core/admin/admin/src/pages/UseCasePage.tsx
#	packages/core/admin/ee/server/src/services/metrics.ts
#	packages/core/admin/package.json
#	packages/core/admin/server/src/bootstrap.ts
#	packages/core/content-manager/package.json
#	packages/core/content-releases/admin/src/components/CMReleasesContainer.tsx
#	packages/core/content-releases/admin/src/components/ReleaseListCell.tsx
#	packages/core/content-releases/admin/src/index.ts
#	packages/core/content-releases/admin/src/pages/ReleaseDetailsPage.tsx
#	packages/core/content-releases/admin/src/pages/tests/ReleaseDetailsPage.test.tsx
#	packages/core/content-releases/admin/src/services/release.ts
#	packages/core/content-releases/package.json
#	packages/core/content-releases/server/src/controllers/release.ts
#	packages/core/content-type-builder/package.json
#	packages/core/core/.gitignore
#	packages/core/data-transfer/package.json
#	packages/core/database/package.json
#	packages/core/database/src/query/helpers/populate/apply.ts
#	packages/core/email/package.json
#	packages/core/helper-plugin/package.json
#	packages/core/permissions/package.json
#	packages/core/strapi/package.json
#	packages/core/types/package.json
#	packages/core/upload/admin/src/components/AssetDialog/BrowseStep/tests/__snapshots__/index.test.jsx.snap
#	packages/core/upload/admin/src/pages/App/ConfigureTheView/components/tests/__snapshots__/Settings.test.jsx.snap
#	packages/core/upload/admin/src/pages/App/ConfigureTheView/tests/__snapshots__/ConfigureTheView.test.jsx.snap
#	packages/core/upload/package.json
#	packages/core/upload/server/services/provider.js
#	packages/core/upload/server/services/upload.js
#	packages/core/upload/server/src/services/image-manipulation.ts
#	packages/core/upload/server/tsconfig.eslint.json
#	packages/core/utils/package.json
#	packages/generators/app/package.json
#	packages/generators/app/src/utils/db-client-dependencies.ts
#	packages/generators/generators/package.json
#	packages/plugins/cloud/package.json
#	packages/plugins/color-picker/package.json
#	packages/plugins/documentation/package.json
#	packages/plugins/graphql/package.json
#	packages/plugins/i18n/admin/src/components/LocaleListCell.tsx
#	packages/plugins/i18n/admin/src/contentReleasesHooks/releaseDetailsView.ts
#	packages/plugins/i18n/admin/src/index.ts
#	packages/plugins/i18n/package.json
#	packages/plugins/sentry/package.json
#	packages/plugins/users-permissions/package.json
#	packages/providers/email-amazon-ses/package.json
#	packages/providers/email-mailgun/package.json
#	packages/providers/email-nodemailer/package.json
#	packages/providers/email-sendgrid/package.json
#	packages/providers/email-sendmail/package.json
#	packages/providers/upload-aws-s3/package.json
#	packages/providers/upload-cloudinary/package.json
#	packages/providers/upload-local/package.json
#	packages/utils/api-tests/package.json
#	packages/utils/eslint-config-custom/package.json
#	packages/utils/logger/package.json
#	packages/utils/tsconfig/package.json
#	packages/utils/typescript/package.json
#	packages/utils/upgrade/LICENSE
#	packages/utils/upgrade/bin/upgrade.js
#	packages/utils/upgrade/package.json
#	packages/utils/upgrade/packup.config.ts
#	scripts/front/package.json
#	tests/e2e/constants.ts
#	tests/e2e/tests/content-releases/releases-page.spec.ts
#	yarn.lock
2024-05-30 13:12:27 +02:00

442 lines
13 KiB
JavaScript

'use strict';
/**
* Auth.js controller
*
* @description: A set of functions called "actions" for managing `Auth`.
*/
/* eslint-disable no-useless-escape */
const crypto = require('crypto');
const _ = require('lodash');
const { concat, compact, isArray } = require('lodash/fp');
const utils = require('@strapi/utils');
const { getService } = require('../utils');
const {
validateCallbackBody,
validateRegisterBody,
validateSendEmailConfirmationBody,
validateForgotPasswordBody,
validateResetPasswordBody,
validateEmailConfirmationBody,
validateChangePasswordBody,
} = require('./validation/auth');
const { ApplicationError, ValidationError, ForbiddenError } = utils.errors;
const sanitizeUser = (user, ctx) => {
const { auth } = ctx.state;
const userSchema = strapi.getModel('plugin::users-permissions.user');
return strapi.contentAPI.sanitize.output(user, userSchema, { auth });
};
module.exports = {
async callback(ctx) {
const provider = ctx.params.provider || 'local';
const params = ctx.request.body;
const store = strapi.store({ type: 'plugin', name: 'users-permissions' });
const grantSettings = await store.get({ key: 'grant' });
const grantProvider = provider === 'local' ? 'email' : provider;
if (!_.get(grantSettings, [grantProvider, 'enabled'])) {
throw new ApplicationError('This provider is disabled');
}
if (provider === 'local') {
await validateCallbackBody(params);
const { identifier } = params;
// Check if the user exists.
const user = await strapi.db.query('plugin::users-permissions.user').findOne({
where: {
provider,
$or: [{ email: identifier.toLowerCase() }, { username: identifier }],
},
});
if (!user) {
throw new ValidationError('Invalid identifier or password');
}
if (!user.password) {
throw new ValidationError('Invalid identifier or password');
}
const validPassword = await getService('user').validatePassword(
params.password,
user.password
);
if (!validPassword) {
throw new ValidationError('Invalid identifier or password');
}
const advancedSettings = await store.get({ key: 'advanced' });
const requiresConfirmation = _.get(advancedSettings, 'email_confirmation');
if (requiresConfirmation && user.confirmed !== true) {
throw new ApplicationError('Your account email is not confirmed');
}
if (user.blocked === true) {
throw new ApplicationError('Your account has been blocked by an administrator');
}
return ctx.send({
jwt: getService('jwt').issue({ id: user.id }),
user: await sanitizeUser(user, ctx),
});
}
// Connect the user with the third-party provider.
try {
const user = await getService('providers').connect(provider, ctx.query);
if (user.blocked) {
throw new ForbiddenError('Your account has been blocked by an administrator');
}
return ctx.send({
jwt: getService('jwt').issue({ id: user.id }),
user: await sanitizeUser(user, ctx),
});
} catch (error) {
throw new ApplicationError(error.message);
}
},
async changePassword(ctx) {
if (!ctx.state.user) {
throw new ApplicationError('You must be authenticated to reset your password');
}
const { currentPassword, password } = await validateChangePasswordBody(ctx.request.body);
const user = await strapi.db
.query('plugin::users-permissions.user')
.findOne({ where: { id: ctx.state.user.id } });
const validPassword = await getService('user').validatePassword(currentPassword, user.password);
if (!validPassword) {
throw new ValidationError('The provided current password is invalid');
}
if (currentPassword === password) {
throw new ValidationError('Your new password must be different than your current password');
}
await getService('user').edit(user.id, { password });
ctx.send({
jwt: getService('jwt').issue({ id: user.id }),
user: await sanitizeUser(user, ctx),
});
},
async resetPassword(ctx) {
const { password, passwordConfirmation, code } = await validateResetPasswordBody(
ctx.request.body
);
if (password !== passwordConfirmation) {
throw new ValidationError('Passwords do not match');
}
const user = await strapi.db
.query('plugin::users-permissions.user')
.findOne({ where: { resetPasswordToken: code } });
if (!user) {
throw new ValidationError('Incorrect code provided');
}
await getService('user').edit(user.id, {
resetPasswordToken: null,
password,
});
// Update the user.
ctx.send({
jwt: getService('jwt').issue({ id: user.id }),
user: await sanitizeUser(user, ctx),
});
},
async connect(ctx, next) {
const grant = require('grant-koa');
const providers = await strapi
.store({ type: 'plugin', name: 'users-permissions', key: 'grant' })
.get();
const apiPrefix = strapi.config.get('api.rest.prefix');
const grantConfig = {
defaults: {
prefix: `${apiPrefix}/connect`,
},
...providers,
};
const [requestPath] = ctx.request.url.split('?');
const provider = requestPath.split('/connect/')[1].split('/')[0];
if (!_.get(grantConfig[provider], 'enabled')) {
throw new ApplicationError('This provider is disabled');
}
if (!strapi.config.server.url.startsWith('http')) {
strapi.log.warn(
'You are using a third party provider for login. Make sure to set an absolute url in config/server.js. More info here: https://docs.strapi.io/developer-docs/latest/plugins/users-permissions.html#setting-up-the-server-url'
);
}
// Ability to pass OAuth callback dynamically
const queryCustomCallback = _.get(ctx, 'query.callback');
const dynamicSessionCallback = _.get(ctx, 'session.grant.dynamic.callback');
const customCallback = queryCustomCallback ?? dynamicSessionCallback;
// The custom callback is validated to make sure it's not redirecting to an unwanted actor.
if (customCallback !== undefined) {
try {
// We're extracting the callback validator from the plugin config since it can be user-customized
const { validate: validateCallback } = strapi
.plugin('users-permissions')
.config('callback');
await validateCallback(customCallback, grantConfig[provider]);
grantConfig[provider].callback = customCallback;
} catch (e) {
throw new ValidationError('Invalid callback URL provided', { callback: customCallback });
}
}
// Build a valid redirect URI for the current provider
grantConfig[provider].redirect_uri = getService('providers').buildRedirectUri(provider);
return grant(grantConfig)(ctx, next);
},
async forgotPassword(ctx) {
const { email } = await validateForgotPasswordBody(ctx.request.body);
const pluginStore = await strapi.store({ type: 'plugin', name: 'users-permissions' });
const emailSettings = await pluginStore.get({ key: 'email' });
const advancedSettings = await pluginStore.get({ key: 'advanced' });
// Find the user by email.
const user = await strapi.db
.query('plugin::users-permissions.user')
.findOne({ where: { email: email.toLowerCase() } });
if (!user || user.blocked) {
return ctx.send({ ok: true });
}
// Generate random token.
const userInfo = await sanitizeUser(user, ctx);
const resetPasswordToken = crypto.randomBytes(64).toString('hex');
const resetPasswordSettings = _.get(emailSettings, 'reset_password.options', {});
const emailBody = await getService('users-permissions').template(
resetPasswordSettings.message,
{
URL: advancedSettings.email_reset_password,
SERVER_URL: strapi.config.get('server.absoluteUrl'),
ADMIN_URL: strapi.config.get('admin.absoluteUrl'),
USER: userInfo,
TOKEN: resetPasswordToken,
}
);
const emailObject = await getService('users-permissions').template(
resetPasswordSettings.object,
{
USER: userInfo,
}
);
const emailToSend = {
to: user.email,
from:
resetPasswordSettings.from.email || resetPasswordSettings.from.name
? `${resetPasswordSettings.from.name} <${resetPasswordSettings.from.email}>`
: undefined,
replyTo: resetPasswordSettings.response_email,
subject: emailObject,
text: emailBody,
html: emailBody,
};
// NOTE: Update the user before sending the email so an Admin can generate the link if the email fails
await getService('user').edit(user.id, { resetPasswordToken });
// Send an email to the user.
await strapi.plugin('email').service('email').send(emailToSend);
ctx.send({ ok: true });
},
async register(ctx) {
const pluginStore = await strapi.store({ type: 'plugin', name: 'users-permissions' });
const settings = await pluginStore.get({ key: 'advanced' });
if (!settings.allow_register) {
throw new ApplicationError('Register action is currently disabled');
}
const { register } = strapi.config.get('plugin::users-permissions');
const alwaysAllowedKeys = ['username', 'password', 'email'];
// Note that we intentionally do not filter allowedFields to allow a project to explicitly accept private or other Strapi field on registration
const allowedKeys = compact(
concat(alwaysAllowedKeys, isArray(register?.allowedFields) ? register.allowedFields : [])
);
// Check if there are any keys in requestBody that are not in allowedKeys
const invalidKeys = Object.keys(ctx.request.body).filter((key) => !allowedKeys.includes(key));
if (invalidKeys.length > 0) {
// If there are invalid keys, throw an error
throw new ValidationError(`Invalid parameters: ${invalidKeys.join(', ')}`);
}
const params = {
..._.pick(ctx.request.body, allowedKeys),
provider: 'local',
};
await validateRegisterBody(params);
const role = await strapi.db
.query('plugin::users-permissions.role')
.findOne({ where: { type: settings.default_role } });
if (!role) {
throw new ApplicationError('Impossible to find the default role');
}
const { email, username, provider } = params;
const identifierFilter = {
$or: [
{ email: email.toLowerCase() },
{ username: email.toLowerCase() },
{ username },
{ email: username },
],
};
const conflictingUserCount = await strapi.db.query('plugin::users-permissions.user').count({
where: { ...identifierFilter, provider },
});
if (conflictingUserCount > 0) {
throw new ApplicationError('Email or Username are already taken');
}
if (settings.unique_email) {
const conflictingUserCount = await strapi.db.query('plugin::users-permissions.user').count({
where: { ...identifierFilter },
});
if (conflictingUserCount > 0) {
throw new ApplicationError('Email or Username are already taken');
}
}
const newUser = {
...params,
role: role.id,
email: email.toLowerCase(),
username,
confirmed: !settings.email_confirmation,
};
const user = await getService('user').add(newUser);
const sanitizedUser = await sanitizeUser(user, ctx);
if (settings.email_confirmation) {
try {
await getService('user').sendConfirmationEmail(sanitizedUser);
} catch (err) {
throw new ApplicationError(err.message);
}
return ctx.send({ user: sanitizedUser });
}
const jwt = getService('jwt').issue(_.pick(user, ['id']));
return ctx.send({
jwt,
user: sanitizedUser,
});
},
async emailConfirmation(ctx, next, returnUser) {
const { confirmation: confirmationToken } = await validateEmailConfirmationBody(ctx.query);
const userService = getService('user');
const jwtService = getService('jwt');
const [user] = await userService.fetchAll({ filters: { confirmationToken } });
if (!user) {
throw new ValidationError('Invalid token');
}
await userService.edit(user.id, { confirmed: true, confirmationToken: null });
if (returnUser) {
ctx.send({
jwt: jwtService.issue({ id: user.id }),
user: await sanitizeUser(user, ctx),
});
} else {
const settings = await strapi
.store({ type: 'plugin', name: 'users-permissions', key: 'advanced' })
.get();
ctx.redirect(settings.email_confirmation_redirection || '/');
}
},
async sendEmailConfirmation(ctx) {
const { email } = await validateSendEmailConfirmationBody(ctx.request.body);
const user = await strapi.db.query('plugin::users-permissions.user').findOne({
where: { email: email.toLowerCase() },
});
if (!user) {
return ctx.send({ email, sent: true });
}
if (user.confirmed) {
throw new ApplicationError('Already confirmed');
}
if (user.blocked) {
throw new ApplicationError('User blocked');
}
await getService('user').sendConfirmationEmail(user);
ctx.send({
email: user.email,
sent: true,
});
},
};