Files
Umbraco-CMS/src/Umbraco.Web.BackOffice/Controllers/UsersController.cs

948 lines
36 KiB
C#
Raw Normal View History

Merge remote-tracking branch 'origin/v8/dev' into netcore/feature/merge-v8-18-01-2021 # Conflicts: # .gitignore # build/NuSpecs/UmbracoCms.Core.nuspec # src/SolutionInfo.cs # src/Umbraco.Core/Configuration/UmbracoSettings/BackOfficeElement.cs # src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs # src/Umbraco.Core/Configuration/UmbracoSettings/IBackOfficeSection.cs # src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs # src/Umbraco.Core/IO/SystemFiles.cs # src/Umbraco.Core/Models/ContentBase.cs # src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs # src/Umbraco.Core/Persistence/UmbracoDatabaseExtensions.cs # src/Umbraco.Core/Runtime/CoreRuntime.cs # src/Umbraco.Core/RuntimeOptions.cs # src/Umbraco.Core/RuntimeState.cs # src/Umbraco.Core/Telemetry/TelemetryMarkerComponent.cs # src/Umbraco.Core/Telemetry/TelemetryMarkerComposer.cs # src/Umbraco.Examine/Umbraco.Examine.csproj # src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs # src/Umbraco.Infrastructure/Install/InstallStepCollection.cs # src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs # src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs # src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs # src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs # src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs # src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs # src/Umbraco.Tests/Runtimes/StandaloneTests.cs # src/Umbraco.Tests/Testing/TestDatabase.cs # src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs # src/Umbraco.Web.UI.Client/src/installer/steps/database.controller.js # src/Umbraco.Web.UI.NetCore/Views/Partials/Grid/Editors/TextString.cshtml # src/Umbraco.Web.UI.NetCore/umbraco/config/lang/da.xml # src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en.xml # src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en_us.xml # src/Umbraco.Web.UI/Umbraco/config/lang/cs.xml # src/Umbraco.Web.UI/config/umbracoSettings.Release.config # src/Umbraco.Web/Composing/CompositionExtensions/Installer.cs # src/Umbraco.Web/Editors/PreviewController.cs # src/Umbraco.Web/Editors/UsersController.cs # src/Umbraco.Web/JavaScript/PreviewInitialize.js # src/Umbraco.Web/Telemetry/TelemetryComponent.cs # src/Umbraco.Web/UmbracoApplication.cs
2021-01-18 15:40:22 +01:00
using System.Globalization;
2017-09-12 16:22:16 +02:00
using System.Net;
using System.Runtime.Serialization;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MimeKit;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Editors;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Mail;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Media;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
2021-06-04 09:50:49 +02:00
using Umbraco.Cms.Core.Models.Email;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Web.BackOffice.Filters;
using Umbraco.Cms.Web.BackOffice.ModelBinders;
using Umbraco.Cms.Web.BackOffice.Security;
2021-02-10 11:42:04 +01:00
using Umbraco.Cms.Web.Common.ActionsResults;
using Umbraco.Cms.Web.Common.Attributes;
using Umbraco.Cms.Web.Common.Authorization;
2021-02-26 14:21:23 +00:00
using Umbraco.Cms.Web.Common.Security;
2020-05-18 13:00:32 +01:00
using Umbraco.Extensions;
namespace Umbraco.Cms.Web.BackOffice.Controllers;
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
[Authorize(Policy = AuthorizationPolicies.SectionAccessUsers)]
[PrefixlessBodyModelValidator]
[IsCurrentUserModelFilter]
public class UsersController : BackOfficeNotificationsController
2017-09-12 16:22:16 +02:00
{
private readonly AppCaches _appCaches;
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
private readonly ContentSettings _contentSettings;
private readonly IEmailSender _emailSender;
private readonly IBackOfficeExternalLoginProviders _externalLogins;
private readonly GlobalSettings _globalSettings;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IImageUrlGenerator _imageUrlGenerator;
private readonly LinkGenerator _linkGenerator;
private readonly ILocalizedTextService _localizedTextService;
private readonly ILogger<UsersController> _logger;
private readonly ILoggerFactory _loggerFactory;
private readonly MediaFileManager _mediaFileManager;
private readonly IPasswordChanger<BackOfficeIdentityUser> _passwordChanger;
private readonly SecuritySettings _securitySettings;
private readonly IShortStringHelper _shortStringHelper;
private readonly ISqlContext _sqlContext;
private readonly IUmbracoMapper _umbracoMapper;
private readonly UserEditorAuthorizationHelper _userEditorAuthorizationHelper;
private readonly IBackOfficeUserManager _userManager;
private readonly IUserService _userService;
private readonly WebRoutingSettings _webRoutingSettings;
[ActivatorUtilitiesConstructor]
public UsersController(
MediaFileManager mediaFileManager,
IOptionsSnapshot<ContentSettings> contentSettings,
IHostingEnvironment hostingEnvironment,
ISqlContext sqlContext,
IImageUrlGenerator imageUrlGenerator,
IOptionsSnapshot<SecuritySettings> securitySettings,
IEmailSender emailSender,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
AppCaches appCaches,
IShortStringHelper shortStringHelper,
IUserService userService,
ILocalizedTextService localizedTextService,
IUmbracoMapper umbracoMapper,
IOptionsSnapshot<GlobalSettings> globalSettings,
IBackOfficeUserManager backOfficeUserManager,
ILoggerFactory loggerFactory,
LinkGenerator linkGenerator,
IBackOfficeExternalLoginProviders externalLogins,
UserEditorAuthorizationHelper userEditorAuthorizationHelper,
IPasswordChanger<BackOfficeIdentityUser> passwordChanger,
IHttpContextAccessor httpContextAccessor,
IOptions<WebRoutingSettings> webRoutingSettings)
{
_mediaFileManager = mediaFileManager;
_contentSettings = contentSettings.Value;
_hostingEnvironment = hostingEnvironment;
_sqlContext = sqlContext;
_imageUrlGenerator = imageUrlGenerator;
_securitySettings = securitySettings.Value;
_emailSender = emailSender;
_backofficeSecurityAccessor = backofficeSecurityAccessor;
_appCaches = appCaches;
_shortStringHelper = shortStringHelper;
_userService = userService;
_localizedTextService = localizedTextService;
_umbracoMapper = umbracoMapper;
_globalSettings = globalSettings.Value;
_userManager = backOfficeUserManager;
_loggerFactory = loggerFactory;
_linkGenerator = linkGenerator;
_externalLogins = externalLogins;
_userEditorAuthorizationHelper = userEditorAuthorizationHelper;
_passwordChanger = passwordChanger;
_logger = _loggerFactory.CreateLogger<UsersController>();
_httpContextAccessor = httpContextAccessor;
_webRoutingSettings = webRoutingSettings.Value;
}
/// <summary>
/// Returns a list of the sizes of gravatar URLs for the user or null if the gravatar server cannot be reached
/// </summary>
/// <returns></returns>
public ActionResult<string[]> GetCurrentUserAvatarUrls()
2017-09-12 16:22:16 +02:00
{
var urls = _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.GetUserAvatarUrls(
_appCaches.RuntimeCache, _mediaFileManager, _imageUrlGenerator);
if (urls == null)
{
return ValidationProblem("Could not access Gravatar endpoint");
}
return urls;
}
[AppendUserModifiedHeader("id")]
[Authorize(Policy = AuthorizationPolicies.AdminUserEditsRequireAdmin)]
public IActionResult PostSetAvatar(int id, IList<IFormFile> file) => PostSetAvatarInternal(file, _userService,
_appCaches.RuntimeCache, _mediaFileManager, _shortStringHelper, _contentSettings, _hostingEnvironment,
_imageUrlGenerator, id);
internal static IActionResult PostSetAvatarInternal(IList<IFormFile> files, IUserService userService,
IAppCache cache, MediaFileManager mediaFileManager, IShortStringHelper shortStringHelper,
ContentSettings contentSettings, IHostingEnvironment hostingEnvironment, IImageUrlGenerator imageUrlGenerator,
int id)
{
if (files is null)
2017-09-12 16:22:16 +02:00
{
return new UnsupportedMediaTypeResult();
}
var root = hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads);
//ensure it exists
Directory.CreateDirectory(root);
2017-09-12 16:22:16 +02:00
//must have a file
if (files.Count == 0)
{
return new NotFoundResult();
2017-09-12 16:22:16 +02:00
}
IUser? user = userService.GetUserById(id);
if (user == null)
2017-09-12 16:22:16 +02:00
{
return new NotFoundResult();
2017-09-12 16:22:16 +02:00
}
if (files.Count > 1)
2017-09-12 16:22:16 +02:00
{
return new ValidationErrorResult(
"The request was not formatted correctly, only one file can be attached to the request");
}
//get the file info
IFormFile file = files.First();
var fileName = file.FileName.Trim(new[] { '\"' }).TrimEnd();
var safeFileName = fileName.ToSafeFileName(shortStringHelper);
var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLower();
const string allowedAvatarFileTypes = "jpeg,jpg,gif,bmp,png,tiff,tif,webp";
2017-09-12 16:22:16 +02:00
if (allowedAvatarFileTypes.Contains(ext) == true && contentSettings.DisallowedUploadFiles.Contains(ext) == false)
{
//generate a path of known data, we don't want this path to be guessable
user.Avatar = "UserAvatars/" + (user.Id + safeFileName).GenerateHash<SHA1>() + "." + ext;
2017-09-12 16:22:16 +02:00
using (Stream fs = file.OpenReadStream())
2017-09-12 16:22:16 +02:00
{
mediaFileManager.FileSystem.AddFile(user.Avatar, fs, true);
2017-09-12 16:22:16 +02:00
}
userService.Save(user);
}
2017-09-12 16:22:16 +02:00
return new OkObjectResult(user.GetUserAvatarUrls(cache, mediaFileManager, imageUrlGenerator));
}
2017-09-12 16:22:16 +02:00
[AppendUserModifiedHeader("id")]
[Authorize(Policy = AuthorizationPolicies.AdminUserEditsRequireAdmin)]
public ActionResult<string[]> PostClearAvatar(int id)
{
IUser? found = _userService.GetUserById(id);
if (found == null)
{
return NotFound();
}
2017-09-12 16:22:16 +02:00
var filePath = found.Avatar;
2017-09-12 16:22:16 +02:00
//if the filePath is already null it will mean that the user doesn't have a custom avatar and their gravatar is currently
//being used (if they have one). This means they want to remove their gravatar too which we can do by setting a special value
//for the avatar.
if (filePath.IsNullOrWhiteSpace() == false)
{
found.Avatar = null;
2017-09-12 16:22:16 +02:00
}
else
2017-09-12 16:22:16 +02:00
{
//set a special value to indicate to not have any avatar
found.Avatar = "none";
}
2017-09-12 16:22:16 +02:00
_userService.Save(found);
2017-09-12 16:22:16 +02:00
if (filePath.IsNullOrWhiteSpace() == false)
{
if (_mediaFileManager.FileSystem.FileExists(filePath!))
2017-09-12 16:22:16 +02:00
{
_mediaFileManager.FileSystem.DeleteFile(filePath!);
2017-09-12 16:22:16 +02:00
}
}
2017-09-12 16:22:16 +02:00
return found.GetUserAvatarUrls(_appCaches.RuntimeCache, _mediaFileManager, _imageUrlGenerator);
}
2017-09-12 16:22:16 +02:00
/// <summary>
/// Gets a user by Id
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[OutgoingEditorModelEvent]
[Authorize(Policy = AuthorizationPolicies.AdminUserEditsRequireAdmin)]
public ActionResult<UserDisplay?> GetById(int id)
{
IUser? user = _userService.GetUserById(id);
if (user == null)
{
return NotFound();
2017-09-12 16:22:16 +02:00
}
UserDisplay? result = _umbracoMapper.Map<IUser, UserDisplay>(user);
return result;
}
/// <summary>
/// Get users by integer ids
/// </summary>
/// <param name="ids"></param>
/// <returns></returns>
[OutgoingEditorModelEvent]
[Authorize(Policy = AuthorizationPolicies.AdminUserEditsRequireAdmin)]
public ActionResult<IEnumerable<UserDisplay?>> GetByIds([FromJsonPath] int[] ids)
{
if (ids == null)
2017-09-12 16:22:16 +02:00
{
return NotFound();
2017-09-12 16:22:16 +02:00
}
if (ids.Length == 0)
{
return Enumerable.Empty<UserDisplay>().ToList();
}
IEnumerable<IUser>? users = _userService.GetUsersById(ids);
if (users == null)
{
return NotFound();
}
List<UserDisplay> result = _umbracoMapper.MapEnumerable<IUser, UserDisplay>(users);
return result;
}
/// <summary>
/// Returns a paged users collection
/// </summary>
/// <param name="pageNumber"></param>
/// <param name="pageSize"></param>
/// <param name="orderBy"></param>
/// <param name="orderDirection"></param>
/// <param name="userGroups"></param>
/// <param name="userStates"></param>
/// <param name="filter"></param>
/// <returns></returns>
public PagedUserResult GetPagedUsers(
int pageNumber = 1,
int pageSize = 10,
string orderBy = "username",
Direction orderDirection = Direction.Ascending,
[FromQuery] string[]? userGroups = null,
[FromQuery] UserState[]? userStates = null,
string filter = "")
{
//following the same principle we had in previous versions, we would only show admins to admins, see
// https://github.com/umbraco/Umbraco-CMS/blob/dev-v7/src/Umbraco.Web/umbraco.presentation/umbraco/Trees/loadUsers.cs#L91
// so to do that here, we'll need to check if this current user is an admin and if not we should exclude all user who are
// also admins
var hideDisabledUsers = _securitySettings.HideDisabledUsersInBackOffice;
var excludeUserGroups = new string[0];
var isAdmin = _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.IsAdmin();
if (isAdmin == false)
{
//this user is not an admin so in that case we need to exclude all admin users
excludeUserGroups = new[] { Constants.Security.AdminGroupAlias };
}
IQuery<IUser> filterQuery = _sqlContext.Query<IUser>();
2017-09-19 15:51:47 +02:00
if (!_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.IsSuper() ?? false)
{
// only super can see super - but don't use IsSuper, cannot be mapped to SQL
//filterQuery.Where(x => !x.IsSuper());
filterQuery.Where(x => x.Id != Constants.Security.SuperUserId);
}
2017-09-19 15:51:47 +02:00
if (filter.IsNullOrWhiteSpace() == false)
{
filterQuery.Where(x => x.Name!.Contains(filter) || x.Username.Contains(filter));
}
2017-09-19 15:51:47 +02:00
if (hideDisabledUsers)
{
if (userStates == null || userStates.Any() == false)
2017-09-19 15:51:47 +02:00
{
userStates = new[] { UserState.Active, UserState.Invited, UserState.LockedOut, UserState.Inactive };
2017-09-19 15:51:47 +02:00
}
}
2017-09-19 15:51:47 +02:00
long pageIndex = pageNumber - 1;
IEnumerable<IUser> result = _userService.GetAll(pageIndex, pageSize, out long total, orderBy, orderDirection,
userStates, userGroups, excludeUserGroups, filterQuery);
var paged = new PagedUserResult(total, pageNumber, pageSize)
{
Items = _umbracoMapper.MapEnumerable<IUser, UserBasic>(result).WhereNotNull(),
UserStates = _userService.GetUserStates()
};
2017-09-19 15:51:47 +02:00
return paged;
}
2017-09-12 16:22:16 +02:00
/// <summary>
/// Creates a new user
/// </summary>
/// <param name="userSave"></param>
/// <returns></returns>
public async Task<ActionResult<UserDisplay?>> PostCreateUser(UserInvite userSave)
{
if (userSave == null)
{
throw new ArgumentNullException("userSave");
2017-09-12 16:22:16 +02:00
}
if (ModelState.IsValid == false)
2017-09-12 16:22:16 +02:00
{
return ValidationProblem(ModelState);
}
2017-09-12 16:22:16 +02:00
if (_securitySettings.UsernameIsEmail)
{
//ensure they are the same if we're using it
userSave.Username = userSave.Email;
}
else
{
//first validate the username if were showing it
CheckUniqueUsername(userSave.Username, null);
}
2017-09-12 16:22:16 +02:00
CheckUniqueEmail(userSave.Email, null);
2017-09-19 15:51:47 +02:00
if (ModelState.IsValid == false)
{
return ValidationProblem(ModelState);
}
//Perform authorization here to see if the current user can actually save this user with the info being requested
Attempt<string?> canSaveUser = _userEditorAuthorizationHelper.IsAuthorized(
_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser, null, null, null, userSave.UserGroups);
if (canSaveUser == false)
{
return Unauthorized(canSaveUser.Result);
}
2017-09-12 16:22:16 +02:00
//we want to create the user with the UserManager, this ensures the 'empty' (special) password
//format is applied without us having to duplicate that logic
var identityUser = BackOfficeIdentityUser.CreateNew(_globalSettings, userSave.Username, userSave.Email,
_globalSettings.DefaultUILanguage);
identityUser.Name = userSave.Name;
2017-09-12 16:22:16 +02:00
IdentityResult created = await _userManager.CreateAsync(identityUser);
if (created.Succeeded == false)
{
return ValidationProblem(created.Errors.ToErrorMessage());
}
2017-09-12 16:22:16 +02:00
string resetPassword;
var password = _userManager.GeneratePassword();
2017-09-12 16:22:16 +02:00
IdentityResult result = await _userManager.AddPasswordAsync(identityUser, password);
if (result.Succeeded == false)
{
return ValidationProblem(created.Errors.ToErrorMessage());
}
2017-09-12 16:22:16 +02:00
resetPassword = password;
//now re-look the user back up which will now exist
IUser? user = _userService.GetByEmail(userSave.Email);
2017-09-12 16:22:16 +02:00
//map the save info over onto the user
user = _umbracoMapper.Map(userSave, user);
2017-09-12 16:22:16 +02:00
if (user is not null)
{
// Since the back office user is creating this user, they will be set to approved
user.IsApproved = true;
2017-09-12 16:22:16 +02:00
_userService.Save(user);
}
2017-09-12 16:22:16 +02:00
UserDisplay? display = _umbracoMapper.Map<UserDisplay>(user);
2022-03-31 15:57:23 +02:00
if (display is not null)
{
display.ResetPasswordValue = resetPassword;
}
return display;
}
2022-03-31 15:57:23 +02:00
/// <summary>
/// Invites a user
/// </summary>
/// <param name="userSave"></param>
/// <returns></returns>
/// <remarks>
/// This will email the user an invite and generate a token that will be validated in the email
/// </remarks>
public async Task<ActionResult<UserDisplay?>> PostInviteUser(UserInvite userSave)
{
if (userSave == null)
{
throw new ArgumentNullException(nameof(userSave));
2017-09-12 16:22:16 +02:00
}
if (userSave.Message.IsNullOrWhiteSpace())
{
ModelState.AddModelError("Message", "Message cannot be empty");
}
2017-09-12 16:22:16 +02:00
if (_securitySettings.UsernameIsEmail)
{
// ensure it's the same
userSave.Username = userSave.Email;
}
else
{
// first validate the username if we're showing it
ActionResult<IUser?> userResult = CheckUniqueUsername(userSave.Username,
u => u.LastLoginDate != default || u.EmailConfirmedDate.HasValue);
if (userResult.Result is not null)
{
return userResult.Result;
}
}
2017-09-12 16:22:16 +02:00
IUser? user = CheckUniqueEmail(userSave.Email,
u => u.LastLoginDate != default || u.EmailConfirmedDate.HasValue);
if (ModelState.IsValid == false)
{
return ValidationProblem(ModelState);
}
Merge branch 'netcore/dev' into netcore/task/6973-migrating-authenticationcontroller # Conflicts: # src/Umbraco.Core/Constants-Security.cs # src/Umbraco.Infrastructure/BackOffice/BackOfficeClaimsPrincipalFactory.cs # src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs # src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs # src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tour/backofficeTour.ts # src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs # src/Umbraco.Web.BackOffice/Controllers/DenyLocalLoginAuthorizationAttribute.cs # src/Umbraco.Web.BackOffice/Controllers/UsersController.cs # src/Umbraco.Web.BackOffice/Extensions/HtmlHelperBackOfficeExtensions.cs # src/Umbraco.Web.BackOffice/Services/IconService.cs # src/Umbraco.Web.Common/Security/ExternalSignInAutoLinkOptions.cs # src/Umbraco.Web.UI.Client/src/common/interceptors/_module.js # src/Umbraco.Web.UI.Client/src/common/interceptors/requiredheaders.interceptor.js # src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js # src/Umbraco.Web.UI.NetCore/umbraco/UmbracoBackOffice/Default.cshtml # src/Umbraco.Web.UI.NetCore/umbraco/config/lang/da.xml # src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en.xml # src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en_us.xml # src/Umbraco.Web.UI/Umbraco/Views/AuthorizeUpgrade.cshtml # src/Umbraco.Web/Editors/AuthenticationController.cs # src/Umbraco.Web/Editors/BackOfficeController.cs # src/Umbraco.Web/Editors/Filters/IsCurrentUserModelFilterAttribute.cs # src/Umbraco.Web/Security/AppBuilderExtensions.cs # src/Umbraco.Web/Security/AuthenticationOptionsExtensions.cs # src/Umbraco.Web/UmbracoDefaultOwinStartup.cs
2020-10-23 10:10:02 +11:00
if (!_emailSender.CanSendRequiredEmail())
{
return ValidationProblem("No Email server is configured");
}
Merge branch 'netcore/dev' into netcore/task/6973-migrating-authenticationcontroller # Conflicts: # src/Umbraco.Core/Constants-Security.cs # src/Umbraco.Infrastructure/BackOffice/BackOfficeClaimsPrincipalFactory.cs # src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs # src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs # src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tour/backofficeTour.ts # src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs # src/Umbraco.Web.BackOffice/Controllers/DenyLocalLoginAuthorizationAttribute.cs # src/Umbraco.Web.BackOffice/Controllers/UsersController.cs # src/Umbraco.Web.BackOffice/Extensions/HtmlHelperBackOfficeExtensions.cs # src/Umbraco.Web.BackOffice/Services/IconService.cs # src/Umbraco.Web.Common/Security/ExternalSignInAutoLinkOptions.cs # src/Umbraco.Web.UI.Client/src/common/interceptors/_module.js # src/Umbraco.Web.UI.Client/src/common/interceptors/requiredheaders.interceptor.js # src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js # src/Umbraco.Web.UI.NetCore/umbraco/UmbracoBackOffice/Default.cshtml # src/Umbraco.Web.UI.NetCore/umbraco/config/lang/da.xml # src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en.xml # src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en_us.xml # src/Umbraco.Web.UI/Umbraco/Views/AuthorizeUpgrade.cshtml # src/Umbraco.Web/Editors/AuthenticationController.cs # src/Umbraco.Web/Editors/BackOfficeController.cs # src/Umbraco.Web/Editors/Filters/IsCurrentUserModelFilterAttribute.cs # src/Umbraco.Web/Security/AppBuilderExtensions.cs # src/Umbraco.Web/Security/AuthenticationOptionsExtensions.cs # src/Umbraco.Web/UmbracoDefaultOwinStartup.cs
2020-10-23 10:10:02 +11:00
// Perform authorization here to see if the current user can actually save this user with the info being requested
Attempt<string?> canSaveUser = _userEditorAuthorizationHelper.IsAuthorized(
_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser, user, null, null, userSave.UserGroups);
if (canSaveUser == false)
{
return ValidationProblem(canSaveUser.Result, StatusCodes.Status401Unauthorized);
}
2017-09-19 15:51:47 +02:00
if (user == null)
{
// we want to create the user with the UserManager, this ensures the 'empty' (special) password
// format is applied without us having to duplicate that logic
var identityUser = BackOfficeIdentityUser.CreateNew(_globalSettings, userSave.Username, userSave.Email,
_globalSettings.DefaultUILanguage);
identityUser.Name = userSave.Name;
2017-09-12 16:22:16 +02:00
IdentityResult created = await _userManager.CreateAsync(identityUser);
if (created.Succeeded == false)
2017-09-12 16:22:16 +02:00
{
return ValidationProblem(created.Errors.ToErrorMessage());
2017-09-12 16:22:16 +02:00
}
// now re-look the user back up
user = _userService.GetByEmail(userSave.Email);
}
2017-09-12 16:22:16 +02:00
// map the save info over onto the user
user = _umbracoMapper.Map(userSave, user);
2022-03-31 15:57:23 +02:00
if (user is not null)
{
// ensure the invited date is set
user.InvitedDate = DateTime.Now;
// Save the updated user (which will process the user groups too)
_userService.Save(user);
}
UserDisplay? display = _umbracoMapper.Map<UserDisplay>(user);
2017-09-12 16:22:16 +02:00
// send the email
await SendUserInviteEmailAsync(display, _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Name,
_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Email, user, userSave.Message);
2017-09-12 16:22:16 +02:00
display?.AddSuccessNotification(_localizedTextService.Localize("speechBubbles", "resendInviteHeader"),
_localizedTextService.Localize("speechBubbles", "resendInviteSuccess", new[] { user?.Name }));
return display;
}
Merge branch 'netcore/dev' into netcore/task/6973-migrating-authenticationcontroller # Conflicts: # src/Umbraco.Core/Constants-Security.cs # src/Umbraco.Infrastructure/BackOffice/BackOfficeClaimsPrincipalFactory.cs # src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs # src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs # src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tour/backofficeTour.ts # src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs # src/Umbraco.Web.BackOffice/Controllers/DenyLocalLoginAuthorizationAttribute.cs # src/Umbraco.Web.BackOffice/Controllers/UsersController.cs # src/Umbraco.Web.BackOffice/Extensions/HtmlHelperBackOfficeExtensions.cs # src/Umbraco.Web.BackOffice/Services/IconService.cs # src/Umbraco.Web.Common/Security/ExternalSignInAutoLinkOptions.cs # src/Umbraco.Web.UI.Client/src/common/interceptors/_module.js # src/Umbraco.Web.UI.Client/src/common/interceptors/requiredheaders.interceptor.js # src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js # src/Umbraco.Web.UI.NetCore/umbraco/UmbracoBackOffice/Default.cshtml # src/Umbraco.Web.UI.NetCore/umbraco/config/lang/da.xml # src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en.xml # src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en_us.xml # src/Umbraco.Web.UI/Umbraco/Views/AuthorizeUpgrade.cshtml # src/Umbraco.Web/Editors/AuthenticationController.cs # src/Umbraco.Web/Editors/BackOfficeController.cs # src/Umbraco.Web/Editors/Filters/IsCurrentUserModelFilterAttribute.cs # src/Umbraco.Web/Security/AppBuilderExtensions.cs # src/Umbraco.Web/Security/AuthenticationOptionsExtensions.cs # src/Umbraco.Web/UmbracoDefaultOwinStartup.cs
2020-10-23 10:10:02 +11:00
private IUser? CheckUniqueEmail(string email, Func<IUser, bool>? extraCheck)
{
IUser? user = _userService.GetByEmail(email);
if (user != null && (extraCheck == null || extraCheck(user)))
{
ModelState.AddModelError("Email", "A user with the email already exists");
2017-09-12 16:22:16 +02:00
}
return user;
}
private ActionResult<IUser?> CheckUniqueUsername(string? username, Func<IUser, bool>? extraCheck)
{
IUser? user = _userService.GetByUsername(username);
if (user != null && (extraCheck == null || extraCheck(user)))
2017-09-19 15:51:47 +02:00
{
ModelState.AddModelError(
_securitySettings.UsernameIsEmail ? "Email" : "Username",
"A user with the username already exists");
return ValidationProblem(ModelState);
2017-09-19 15:51:47 +02:00
}
return new ActionResult<IUser?>(user);
}
V11/feature/update to dotnet 7 (#12712) * Update projects to .NET 7 * Fix nullability errors * Fix up pipelines to run 7.0 * Update langversion to preview * Revert "Fix up pipelines to run 7.0" This reverts commit d0fa8d01b8126a4eaa59832a3814a567705419ae. * Fix up pipelines again, this time without indentation changes * Include preview versions * Versions not Version * Fix ModelTypeTests * Fix MemberPasswordHasherTests Microsoft wants to use SHA512 instead of SHA256, so our old hashes will return SuccessRehashNeeded now * Use dotnet cli instead of nuget restore * Update src/Umbraco.Web.UI/Umbraco.Web.UI.csproj * Update dependencies * Fix nullability issues * Fix unit test * Fix nullability in ChangingPasswordModel OldPassword can be null, if we're changing the password with password reset enabled. Additionally, we might as well use the new required keyword instead of supressing null. * Use required keyword instead of supressing null * Fix up pipelines again * fix up spelling-error * Use dotnet cli instead of nuget restore * Fix up another NuGet command * Use dotnet version 7 before building * Include preview versions * Remove condition * Use dotnet 7 before running powershell script * Update templates to .net 7 * Download version 7 before running linux container * Move use dotnet 7 even earlier in E2E process * Remove dotnet 7 * Reintroduce .NET 7 task * Update linux docker container and remove dotnet 7 from yml * Fix up dockerfile with ARG * Fix up docker file with nightly builds of dotnet 7 * Reintroduce dotnet 7 so windows can use it * Use aspnet 7 in docker Co-authored-by: Nikolaj <nikolajlauridsen@protonmail.ch> Co-authored-by: Zeegaan <nge@umbraco.dk>
2022-08-23 11:31:05 +02:00
private async Task SendUserInviteEmailAsync(UserBasic? userDisplay, string? from, string? fromEmail, IUser? to, string? message)
{
V11/feature/update to dotnet 7 (#12712) * Update projects to .NET 7 * Fix nullability errors * Fix up pipelines to run 7.0 * Update langversion to preview * Revert "Fix up pipelines to run 7.0" This reverts commit d0fa8d01b8126a4eaa59832a3814a567705419ae. * Fix up pipelines again, this time without indentation changes * Include preview versions * Versions not Version * Fix ModelTypeTests * Fix MemberPasswordHasherTests Microsoft wants to use SHA512 instead of SHA256, so our old hashes will return SuccessRehashNeeded now * Use dotnet cli instead of nuget restore * Update src/Umbraco.Web.UI/Umbraco.Web.UI.csproj * Update dependencies * Fix nullability issues * Fix unit test * Fix nullability in ChangingPasswordModel OldPassword can be null, if we're changing the password with password reset enabled. Additionally, we might as well use the new required keyword instead of supressing null. * Use required keyword instead of supressing null * Fix up pipelines again * fix up spelling-error * Use dotnet cli instead of nuget restore * Fix up another NuGet command * Use dotnet version 7 before building * Include preview versions * Remove condition * Use dotnet 7 before running powershell script * Update templates to .net 7 * Download version 7 before running linux container * Move use dotnet 7 even earlier in E2E process * Remove dotnet 7 * Reintroduce .NET 7 task * Update linux docker container and remove dotnet 7 from yml * Fix up dockerfile with ARG * Fix up docker file with nightly builds of dotnet 7 * Reintroduce dotnet 7 so windows can use it * Use aspnet 7 in docker Co-authored-by: Nikolaj <nikolajlauridsen@protonmail.ch> Co-authored-by: Zeegaan <nge@umbraco.dk>
2022-08-23 11:31:05 +02:00
var userId = userDisplay?.Id?.ToString();
if (userId is null)
{
throw new InvalidOperationException("Could not find user Id");
}
var user = await _userManager.FindByIdAsync(userId);
if (user is null)
{
throw new InvalidOperationException("Could not find user");
}
var token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
// Use info from SMTP Settings if configured, otherwise set fromEmail as fallback
var senderEmail = !string.IsNullOrEmpty(_globalSettings.Smtp?.From) ? _globalSettings.Smtp.From : fromEmail;
var inviteToken = string.Format("{0}{1}{2}",
(int?)userDisplay?.Id,
WebUtility.UrlEncode("|"),
token.ToUrlBase64());
// Get an mvc helper to get the URL
var action = _linkGenerator.GetPathByAction(
nameof(BackOfficeController.VerifyInvite),
ControllerExtensions.GetControllerName<BackOfficeController>(),
new { area = Constants.Web.Mvc.BackOfficeArea, invite = inviteToken });
// Construct full URL using configured application URL (which will fall back to request)
Uri applicationUri = _httpContextAccessor.GetRequiredHttpContext().Request
.GetApplicationUri(_webRoutingSettings);
var inviteUri = new Uri(applicationUri, action);
var emailSubject = _localizedTextService.Localize("user", "inviteEmailCopySubject",
// Ensure the culture of the found user is used for the email!
UmbracoUserExtensions.GetUserCulture(to?.Language, _localizedTextService, _globalSettings));
var emailBody = _localizedTextService.Localize("user", "inviteEmailCopyFormat",
// Ensure the culture of the found user is used for the email!
UmbracoUserExtensions.GetUserCulture(to?.Language, _localizedTextService, _globalSettings),
new[] { userDisplay?.Name, from, message, inviteUri.ToString(), senderEmail });
// This needs to be in the correct mailto format including the name, else
// the name cannot be captured in the email sending notification.
// i.e. "Some Person" <hello@example.com>
var toMailBoxAddress = new MailboxAddress(to?.Name, to?.Email);
var mailMessage = new EmailMessage(senderEmail, toMailBoxAddress.ToString(), emailSubject, emailBody, true);
await _emailSender.SendAsync(mailMessage, Constants.Web.EmailTypes.UserInvite, true);
}
/// <summary>
/// Saves a user
/// </summary>
/// <param name="userSave"></param>
/// <returns></returns>
[OutgoingEditorModelEvent]
public ActionResult<UserDisplay?> PostSaveUser(UserSave userSave)
{
if (userSave == null)
2017-09-19 15:51:47 +02:00
{
throw new ArgumentNullException(nameof(userSave));
}
if (ModelState.IsValid == false)
{
return ValidationProblem(ModelState);
2017-09-19 15:51:47 +02:00
}
2017-09-12 16:22:16 +02:00
IUser? found = _userService.GetUserById(userSave.Id);
if (found == null)
2017-09-12 16:22:16 +02:00
{
return NotFound();
}
2017-09-12 16:22:16 +02:00
//Perform authorization here to see if the current user can actually save this user with the info being requested
Attempt<string?> canSaveUser = _userEditorAuthorizationHelper.IsAuthorized(
_backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser, found, userSave.StartContentIds,
userSave.StartMediaIds, userSave.UserGroups);
if (canSaveUser == false)
{
return Unauthorized(canSaveUser.Result);
}
var hasErrors = false;
2017-09-12 16:22:16 +02:00
// we need to check if there's any Deny Local login providers present, if so we need to ensure that the user's email address cannot be changed
var hasDenyLocalLogin = _externalLogins.HasDenyLocalLogin();
if (hasDenyLocalLogin)
{
userSave.Email =
found.Email; // it cannot change, this would only happen if people are mucking around with the request
}
2017-09-12 16:22:16 +02:00
IUser? existing = _userService.GetByEmail(userSave.Email);
if (existing != null && existing.Id != userSave.Id)
{
ModelState.AddModelError("Email", "A user with the email already exists");
hasErrors = true;
}
2017-09-12 16:22:16 +02:00
existing = _userService.GetByUsername(userSave.Username);
if (existing != null && existing.Id != userSave.Id)
{
ModelState.AddModelError("Username", "A user with the username already exists");
hasErrors = true;
}
2017-09-12 16:22:16 +02:00
// going forward we prefer to align usernames with email, so we should cross-check to make sure
// the email or username isn't somehow being used by anyone.
existing = _userService.GetByEmail(userSave.Username);
if (existing != null && existing.Id != userSave.Id)
{
ModelState.AddModelError("Username", "A user using this as their email already exists");
hasErrors = true;
}
existing = _userService.GetByUsername(userSave.Email);
if (existing != null && existing.Id != userSave.Id)
{
ModelState.AddModelError("Email", "A user using this as their username already exists");
hasErrors = true;
}
2017-09-12 16:22:16 +02:00
// if the found user has their email for username, we want to keep this synced when changing the email.
// we have already cross-checked above that the email isn't colliding with anything, so we can safely assign it here.
if (_securitySettings.UsernameIsEmail && found.Username == found.Email && userSave.Username != userSave.Email)
{
userSave.Username = userSave.Email;
2017-09-12 16:22:16 +02:00
}
if (hasErrors)
2017-09-12 16:22:16 +02:00
{
return ValidationProblem(ModelState);
}
2017-09-12 16:22:16 +02:00
//merge the save data onto the user
IUser user = _umbracoMapper.Map(userSave, found);
2017-09-12 16:22:16 +02:00
_userService.Save(user);
2017-09-12 16:22:16 +02:00
UserDisplay? display = _umbracoMapper.Map<UserDisplay>(user);
2017-09-19 15:51:47 +02:00
// determine if the user has changed their own language;
IUser? currentUser = _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser;
var userHasChangedOwnLanguage =
user.Id == currentUser?.Id && currentUser.Language != user.Language;
2017-09-12 16:22:16 +02:00
var textToLocalise = userHasChangedOwnLanguage ? "operationSavedHeaderReloadUser" : "operationSavedHeader";
CultureInfo culture = userHasChangedOwnLanguage
? CultureInfo.GetCultureInfo(user.Language!)
: Thread.CurrentThread.CurrentUICulture;
display?.AddSuccessNotification(_localizedTextService.Localize("speechBubbles", textToLocalise, culture),
_localizedTextService.Localize("speechBubbles", "editUserSaved", culture));
return display;
}
Merge branch 'netcore/dev' into netcore/task/6973-migrating-authenticationcontroller # Conflicts: # src/Umbraco.Core/Constants-Security.cs # src/Umbraco.Infrastructure/BackOffice/BackOfficeClaimsPrincipalFactory.cs # src/Umbraco.Infrastructure/BackOffice/BackOfficeUserManager.cs # src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs # src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tour/backofficeTour.ts # src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs # src/Umbraco.Web.BackOffice/Controllers/DenyLocalLoginAuthorizationAttribute.cs # src/Umbraco.Web.BackOffice/Controllers/UsersController.cs # src/Umbraco.Web.BackOffice/Extensions/HtmlHelperBackOfficeExtensions.cs # src/Umbraco.Web.BackOffice/Services/IconService.cs # src/Umbraco.Web.Common/Security/ExternalSignInAutoLinkOptions.cs # src/Umbraco.Web.UI.Client/src/common/interceptors/_module.js # src/Umbraco.Web.UI.Client/src/common/interceptors/requiredheaders.interceptor.js # src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js # src/Umbraco.Web.UI.NetCore/umbraco/UmbracoBackOffice/Default.cshtml # src/Umbraco.Web.UI.NetCore/umbraco/config/lang/da.xml # src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en.xml # src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en_us.xml # src/Umbraco.Web.UI/Umbraco/Views/AuthorizeUpgrade.cshtml # src/Umbraco.Web/Editors/AuthenticationController.cs # src/Umbraco.Web/Editors/BackOfficeController.cs # src/Umbraco.Web/Editors/Filters/IsCurrentUserModelFilterAttribute.cs # src/Umbraco.Web/Security/AppBuilderExtensions.cs # src/Umbraco.Web/Security/AuthenticationOptionsExtensions.cs # src/Umbraco.Web/UmbracoDefaultOwinStartup.cs
2020-10-23 10:10:02 +11:00
/// <summary>
/// </summary>
/// <param name="changingPasswordModel"></param>
/// <returns></returns>
public async Task<ActionResult<ModelWithNotifications<string?>>> PostChangePassword(
ChangingPasswordModel changingPasswordModel)
{
changingPasswordModel = changingPasswordModel ?? throw new ArgumentNullException(nameof(changingPasswordModel));
2017-09-12 16:22:16 +02:00
if (ModelState.IsValid == false)
{
return ValidationProblem(ModelState);
}
2017-09-12 16:22:16 +02:00
IUser? found = _userService.GetUserById(changingPasswordModel.Id);
if (found == null)
{
return NotFound();
}
2017-09-12 16:22:16 +02:00
IUser? currentUser = _backofficeSecurityAccessor.BackOfficeSecurity?.CurrentUser;
2017-09-12 16:22:16 +02:00
// if it's the current user, the current user cannot reset their own password without providing their old password
if (currentUser?.Username == found.Username && string.IsNullOrEmpty(changingPasswordModel.OldPassword))
{
return ValidationProblem("Password reset is not allowed without providing old password");
}
2017-09-12 16:22:16 +02:00
if ((!currentUser?.IsAdmin() ?? false) && found.IsAdmin())
{
return ValidationProblem("The current user cannot change the password for the specified user");
}
2017-09-12 16:22:16 +02:00
Attempt<PasswordChangedModel?> passwordChangeResult =
await _passwordChanger.ChangePasswordWithIdentityAsync(changingPasswordModel, _userManager);
Merge remote-tracking branch 'origin/v8/dev' into netcore/feature/merge-v8-18-01-2021 # Conflicts: # .gitignore # build/NuSpecs/UmbracoCms.Core.nuspec # src/SolutionInfo.cs # src/Umbraco.Core/Configuration/UmbracoSettings/BackOfficeElement.cs # src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs # src/Umbraco.Core/Configuration/UmbracoSettings/IBackOfficeSection.cs # src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs # src/Umbraco.Core/IO/SystemFiles.cs # src/Umbraco.Core/Models/ContentBase.cs # src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs # src/Umbraco.Core/Persistence/UmbracoDatabaseExtensions.cs # src/Umbraco.Core/Runtime/CoreRuntime.cs # src/Umbraco.Core/RuntimeOptions.cs # src/Umbraco.Core/RuntimeState.cs # src/Umbraco.Core/Telemetry/TelemetryMarkerComponent.cs # src/Umbraco.Core/Telemetry/TelemetryMarkerComposer.cs # src/Umbraco.Examine/Umbraco.Examine.csproj # src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs # src/Umbraco.Infrastructure/Install/InstallStepCollection.cs # src/Umbraco.Infrastructure/Install/InstallSteps/NewInstallStep.cs # src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs # src/Umbraco.Infrastructure/Migrations/Install/DatabaseSchemaCreator.cs # src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs # src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs # src/Umbraco.Tests/Runtimes/CoreRuntimeTests.cs # src/Umbraco.Tests/Runtimes/StandaloneTests.cs # src/Umbraco.Tests/Testing/TestDatabase.cs # src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs # src/Umbraco.Web.UI.Client/src/installer/steps/database.controller.js # src/Umbraco.Web.UI.NetCore/Views/Partials/Grid/Editors/TextString.cshtml # src/Umbraco.Web.UI.NetCore/umbraco/config/lang/da.xml # src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en.xml # src/Umbraco.Web.UI.NetCore/umbraco/config/lang/en_us.xml # src/Umbraco.Web.UI/Umbraco/config/lang/cs.xml # src/Umbraco.Web.UI/config/umbracoSettings.Release.config # src/Umbraco.Web/Composing/CompositionExtensions/Installer.cs # src/Umbraco.Web/Editors/PreviewController.cs # src/Umbraco.Web/Editors/UsersController.cs # src/Umbraco.Web/JavaScript/PreviewInitialize.js # src/Umbraco.Web/Telemetry/TelemetryComponent.cs # src/Umbraco.Web/UmbracoApplication.cs
2021-01-18 15:40:22 +01:00
if (passwordChangeResult.Success)
{
var result = new ModelWithNotifications<string?>(passwordChangeResult.Result?.ResetPassword);
result.AddSuccessNotification(_localizedTextService.Localize("general", "success"),
_localizedTextService.Localize("user", "passwordChangedGeneric"));
return result;
2017-09-19 15:51:47 +02:00
}
2017-09-12 16:22:16 +02:00
if (passwordChangeResult.Result?.ChangeError is not null)
{
foreach (var memberName in passwordChangeResult.Result.ChangeError.MemberNames)
{
ModelState.AddModelError(memberName,
passwordChangeResult.Result.ChangeError.ErrorMessage ?? string.Empty);
}
}
return ValidationProblem(ModelState);
}
/// <summary>
/// Disables the users with the given user ids
/// </summary>
/// <param name="userIds"></param>
[Authorize(Policy = AuthorizationPolicies.AdminUserEditsRequireAdmin)]
public IActionResult PostDisableUsers([FromQuery] int[] userIds)
{
Attempt<int> tryGetCurrentUserId =
_backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId() ?? Attempt<int>.Fail();
if (tryGetCurrentUserId.Success && userIds.Contains(tryGetCurrentUserId.Result))
{
return ValidationProblem("The current user cannot disable itself");
}
IUser[] users = _userService.GetUsersById(userIds).ToArray();
foreach (IUser u in users)
{
u.IsApproved = false;
u.InvitedDate = null;
}
_userService.Save(users);
if (users.Length > 1)
{
return Ok(_localizedTextService.Localize("speechBubbles", "disableUsersSuccess",
new[] { userIds.Length.ToString() }));
}
return Ok(_localizedTextService.Localize("speechBubbles", "disableUserSuccess", new[] { users[0].Name }));
}
/// <summary>
/// Enables the users with the given user ids
/// </summary>
/// <param name="userIds"></param>
[Authorize(Policy = AuthorizationPolicies.AdminUserEditsRequireAdmin)]
public IActionResult PostEnableUsers([FromQuery] int[] userIds)
{
IUser[] users = _userService.GetUsersById(userIds).ToArray();
foreach (IUser u in users)
{
u.IsApproved = true;
}
_userService.Save(users);
if (users.Length > 1)
2017-09-12 16:22:16 +02:00
{
return Ok(
_localizedTextService.Localize("speechBubbles", "enableUsersSuccess",
new[] { userIds.Length.ToString() }));
}
2017-09-12 16:22:16 +02:00
return Ok(
_localizedTextService.Localize("speechBubbles", "enableUserSuccess", new[] { users[0].Name }));
}
2017-09-12 16:22:16 +02:00
/// <summary>
/// Unlocks the users with the given user ids
/// </summary>
/// <param name="userIds"></param>
[Authorize(Policy = AuthorizationPolicies.AdminUserEditsRequireAdmin)]
public async Task<IActionResult> PostUnlockUsers([FromQuery] int[] userIds)
{
if (userIds.Length <= 0)
{
return Ok();
2017-09-12 16:22:16 +02:00
}
var notFound = new List<int>();
foreach (var u in userIds)
2017-09-12 16:22:16 +02:00
{
BackOfficeIdentityUser? user = await _userManager.FindByIdAsync(u.ToString());
if (user == null)
2017-09-12 16:22:16 +02:00
{
notFound.Add(u);
continue;
2017-09-12 16:22:16 +02:00
}
IdentityResult unlockResult =
await _userManager.SetLockoutEndDateAsync(user, DateTimeOffset.Now.AddMinutes(-1));
if (unlockResult.Succeeded == false)
2017-09-12 16:22:16 +02:00
{
return ValidationProblem(
$"Could not unlock for user {u} - error {unlockResult.Errors.ToErrorMessage()}");
2017-09-12 16:22:16 +02:00
}
if (userIds.Length == 1)
2017-09-19 15:51:47 +02:00
{
return Ok(
_localizedTextService.Localize("speechBubbles", "unlockUserSuccess", new[] { user.Name }));
2017-09-19 15:51:47 +02:00
}
2017-09-12 16:22:16 +02:00
}
return Ok(
_localizedTextService.Localize("speechBubbles", "unlockUsersSuccess",
new[] { (userIds.Length - notFound.Count).ToString() }));
}
[Authorize(Policy = AuthorizationPolicies.AdminUserEditsRequireAdmin)]
public IActionResult PostSetUserGroupsOnUsers([FromQuery] string[] userGroupAliases, [FromQuery] int[] userIds)
{
IUser[] users = _userService.GetUsersById(userIds).ToArray();
IReadOnlyUserGroup[] userGroups = _userService.GetUserGroupsByAlias(userGroupAliases)
.Select(x => x.ToReadOnlyGroup()).ToArray();
foreach (IUser u in users)
2017-09-12 16:22:16 +02:00
{
u.ClearGroups();
foreach (IReadOnlyUserGroup userGroup in userGroups)
2017-09-12 16:22:16 +02:00
{
u.AddGroup(userGroup);
2017-09-12 16:22:16 +02:00
}
}
_userService.Save(users);
return Ok(
_localizedTextService.Localize("speechBubbles", "setUserGroupOnUsersSuccess"));
}
/// <summary>
/// Deletes the non-logged in user provided id
/// </summary>
/// <param name="id">User Id</param>
/// <remarks>
/// Limited to users that haven't logged in to avoid issues with related records constrained
/// with a foreign key on the user Id
/// </remarks>
[Authorize(Policy = AuthorizationPolicies.AdminUserEditsRequireAdmin)]
public IActionResult PostDeleteNonLoggedInUser(int id)
{
IUser? user = _userService.GetUserById(id);
if (user == null)
{
return NotFound();
}
// Check user hasn't logged in. If they have they may have made content changes which will mean
// the Id is associated with audit trails, versions etc. and can't be removed.
if (user.LastLoginDate is not null && user.LastLoginDate != default(DateTime))
2017-09-12 16:22:16 +02:00
{
return BadRequest();
2017-09-12 16:22:16 +02:00
}
2017-09-19 15:51:47 +02:00
var userName = user.Name;
_userService.Delete(user, true);
return Ok(
_localizedTextService.Localize("speechBubbles", "deleteUserSuccess", new[] { userName }));
}
public class PagedUserResult : PagedResult<UserBasic>
{
public PagedUserResult(long totalItems, long pageNumber, long pageSize) :
base(totalItems, pageNumber, pageSize) => UserStates = new Dictionary<UserState, int>();
/// <summary>
/// This is basically facets of UserStates key = state, value = count
/// </summary>
[DataMember(Name = "userStates")]
public IDictionary<UserState, int> UserStates { get; set; }
2017-09-12 16:22:16 +02:00
}
2017-09-23 10:08:18 +02:00
}