V10: Migrate member properties to columns on the member table (#12205)

* Add new columns to the member table

* Add missing IsApproved column

* Add joins with nested query

* Add query for selecting new column values from existing members

* Update the member data from the same query

* Make escapes work for sqlite

* Use GetFieldNameForUpdate instead of GetFieldName

* Left join on memberDto

* Remove the now unused property types and data

* Don't create member columns as properties anymore

* Store old properties in fields on member

Also switch the dates to nullable since they can be null

* Map columns when mapping from DTO to Member object

* Display columns in the member content app

* Fix null exception when creating new user

* Hide value if user doesn't have access to sensitive data

* Remove hardcoded member properties

* Obsolete old member alias constants

* Map and persist member properties

* Correctly update LastLogin when member logs in

* Map IsApproved and IsLockedOut when saving member in backoffice

* Update the query mappers for members

* Fix member service tracks dirty changes test

* Remove no longer existing property types from member type builder

* Fix null error in UpdateMemberProperties

* Fix builder tests

* Fix SetupMemberTestData in MemberControllerUnitTests

* Let LastLoginDate be null and handle check in controller

There's no reason to default a perfectly nullable property to default(DateTime)

* Add translation key for is approved and use that instead of constant

* Obsolete old label constants

* Fix whitespace post merge

* Fix up test comments

* Apply suggestions from code review

Co-authored-by: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com>

* Fix member properties always being sensitive

Co-authored-by: Elitsa Marinovska <elm@umbraco.dk>
Co-authored-by: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com>
This commit is contained in:
Mole
2022-04-21 14:47:27 +02:00
committed by GitHub
parent 6d0a0fff9c
commit 7df4f84247
36 changed files with 744 additions and 489 deletions

View File

@@ -1,3 +1,5 @@
using System;
namespace Umbraco.Cms.Core
{
public static partial class Constants
@@ -187,43 +189,49 @@ namespace Umbraco.Cms.Core
/// <summary>
/// Property alias for the Approved boolean of a Member
/// </summary>
[Obsolete("IsApproved is no longer property data, access the property directly on IMember instead, scheduled for removal in V11")]
public const string IsApproved = "umbracoMemberApproved";
[Obsolete("Use the stateApproved translation in the user area instead, scheduled for removal in V11")]
public const string IsApprovedLabel = "Is Approved";
/// <summary>
/// Property alias for the Locked out boolean of a Member
/// </summary>
[Obsolete("IsLockedOut is no longer property data, access the property directly on IMember instead, scheduled for removal in V11")]
public const string IsLockedOut = "umbracoMemberLockedOut";
[Obsolete("Use the stateLockedOut translation in the user area instead, scheduled for removal in V11")]
public const string IsLockedOutLabel = "Is Locked Out";
/// <summary>
/// Property alias for the last date the Member logged in
/// </summary>
[Obsolete("LastLoginDate is no longer property data, access the property directly on IMember instead, scheduled for removal in V11")]
public const string LastLoginDate = "umbracoMemberLastLogin";
[Obsolete("Use the lastLogin translation in the user area instead, scheduled for removal in V11")]
public const string LastLoginDateLabel = "Last Login Date";
/// <summary>
/// Property alias for the last date a Member changed its password
/// </summary>
[Obsolete("LastPasswordChangeDate is no longer property data, access the property directly on IMember instead, scheduled for removal in V11")]
public const string LastPasswordChangeDate = "umbracoMemberLastPasswordChangeDate";
[Obsolete("Use the lastPasswordChangeDate translation in the user area instead, scheduled for removal in V11")]
public const string LastPasswordChangeDateLabel = "Last Password Change Date";
/// <summary>
/// Property alias for the last date a Member was locked out
/// </summary>
[Obsolete("LastLockoutDate is no longer property data, access the property directly on IMember instead, scheduled for removal in V11")]
public const string LastLockoutDate = "umbracoMemberLastLockoutDate";
[Obsolete("Use the lastLockoutDate translation in the user area instead, scheduled for removal in V11")]
public const string LastLockoutDateLabel = "Last Lockout Date";
/// <summary>
/// Property alias for the number of failed login attempts
/// </summary>
[Obsolete("FailedPasswordAttempts is no longer property data, access the property directly on IMember instead, scheduled for removal in V11")]
public const string FailedPasswordAttempts = "umbracoMemberFailedPasswordAttempts";
[Obsolete("Use the failedPasswordAttempts translation in the user area instead, scheduled for removal in V11")]
public const string FailedPasswordAttemptsLabel = "Failed Password Attempts";
/// <summary>

View File

@@ -11,64 +11,16 @@ namespace Umbraco.Cms.Core
{
{
Constants.Conventions.Member.Comments,
new PropertyType(shortStringHelper, Constants.PropertyEditors.Aliases.TextArea, ValueStorageType.Ntext, true,
new PropertyType(
shortStringHelper,
Constants.PropertyEditors.Aliases.TextArea,
ValueStorageType.Ntext,
true,
Constants.Conventions.Member.Comments)
{
Name = Constants.Conventions.Member.CommentsLabel
Name = Constants.Conventions.Member.CommentsLabel,
}
},
{
Constants.Conventions.Member.FailedPasswordAttempts,
new PropertyType(shortStringHelper, Constants.PropertyEditors.Aliases.Label, ValueStorageType.Integer, true,
Constants.Conventions.Member.FailedPasswordAttempts)
{
Name = Constants.Conventions.Member.FailedPasswordAttemptsLabel,
DataTypeId = Constants.DataTypes.LabelInt
}
},
{
Constants.Conventions.Member.IsApproved,
new PropertyType(shortStringHelper, Constants.PropertyEditors.Aliases.Boolean, ValueStorageType.Integer, true,
Constants.Conventions.Member.IsApproved)
{
Name = Constants.Conventions.Member.IsApprovedLabel
}
},
{
Constants.Conventions.Member.IsLockedOut,
new PropertyType(shortStringHelper, Constants.PropertyEditors.Aliases.Boolean, ValueStorageType.Integer, true,
Constants.Conventions.Member.IsLockedOut)
{
Name = Constants.Conventions.Member.IsLockedOutLabel
}
},
{
Constants.Conventions.Member.LastLockoutDate,
new PropertyType(shortStringHelper, Constants.PropertyEditors.Aliases.Label, ValueStorageType.Date, true,
Constants.Conventions.Member.LastLockoutDate)
{
Name = Constants.Conventions.Member.LastLockoutDateLabel,
DataTypeId = Constants.DataTypes.LabelDateTime
}
},
{
Constants.Conventions.Member.LastLoginDate,
new PropertyType(shortStringHelper, Constants.PropertyEditors.Aliases.Label, ValueStorageType.Date, true,
Constants.Conventions.Member.LastLoginDate)
{
Name = Constants.Conventions.Member.LastLoginDateLabel,
DataTypeId = Constants.DataTypes.LabelDateTime
}
},
{
Constants.Conventions.Member.LastPasswordChangeDate,
new PropertyType(shortStringHelper, Constants.PropertyEditors.Aliases.Label, ValueStorageType.Date, true,
Constants.Conventions.Member.LastPasswordChangeDate)
{
Name = Constants.Conventions.Member.LastPasswordChangeDateLabel,
DataTypeId = Constants.DataTypes.LabelDateTime
}
}
};
}
}

View File

@@ -24,6 +24,12 @@ namespace Umbraco.Cms.Core.Models.ContentEditing
[DataMember(Name = "email")]
public string Email { get; set; }
[DataMember(Name = "isLockedOut")]
public bool IsLockedOut { get; set; }
[DataMember(Name = "isApproved")]
public bool IsApproved { get; set; }
//[DataMember(Name = "membershipScenario")]
//public MembershipScenario MembershipScenario { get; set; }

View File

@@ -31,15 +31,11 @@ namespace Umbraco.Cms.Core.Models.ContentEditing
/// </summary>
public string Comments => GetPropertyValue<string>(Constants.Conventions.Member.Comments);
/// <summary>
/// Returns the value from the IsLockedOut property
/// </summary>
public bool IsLockedOut => GetPropertyValue<bool>(Constants.Conventions.Member.IsLockedOut);
[DataMember(Name = "isLockedOut")]
public bool IsLockedOut { get; set; }
/// <summary>
/// Returns the value from the IsApproved property
/// </summary>
public bool IsApproved => GetPropertyValue<bool>(Constants.Conventions.Member.IsApproved);
[DataMember(Name = "isApproved")]
public bool IsApproved { get; set; }
private T GetPropertyValue<T>(string alias)
{

View File

@@ -64,11 +64,11 @@ namespace Umbraco.Cms.Core.Models.ContentEditing
[DataMember(Name = "lastLockoutDate")]
[ReadOnly(true)]
public DateTime LastLockoutDate { get; set; }
public DateTime? LastLockoutDate { get; set; }
[DataMember(Name = "lastPasswordChangeDate")]
[ReadOnly(true)]
public DateTime LastPasswordChangeDate { get; set; }
public DateTime? LastPasswordChangeDate { get; set; }
[DataMember(Name = "createDate")]
[ReadOnly(true)]

View File

@@ -65,13 +65,6 @@ namespace Umbraco.Cms.Core.Models.Mapping
var resolved = base.Map(source, context);
// IMember.IsLockedOut can't be set to true, so make it readonly when that's the case (you can only unlock)
var isLockedOutProperty = resolved.SelectMany(x => x.Properties).FirstOrDefault(x => x.Alias == Constants.Conventions.Member.IsLockedOut);
if (isLockedOutProperty?.Value != null && isLockedOutProperty.Value.ToString() != "1")
{
isLockedOutProperty.Readonly = true;
}
return resolved;
}
@@ -189,7 +182,7 @@ namespace Umbraco.Cms.Core.Models.Mapping
var properties = new List<ContentPropertyDisplay>
{
GetLoginProperty(member, _localizedTextService),
new ContentPropertyDisplay
new()
{
Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email",
Label = _localizedTextService.Localize("general","email"),
@@ -197,7 +190,7 @@ namespace Umbraco.Cms.Core.Models.Mapping
View = "email",
Validation = { Mandatory = true }
},
new ContentPropertyDisplay
new()
{
Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password",
Label = _localizedTextService.Localize(null,"password"),
@@ -209,7 +202,7 @@ namespace Umbraco.Cms.Core.Models.Mapping
View = "changepassword",
Config = GetPasswordConfig(member) // Initialize the dictionary with the configuration from the default membership provider
},
new ContentPropertyDisplay
new()
{
Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}membergroup",
Label = _localizedTextService.Localize("content","membergroup"),
@@ -218,9 +211,80 @@ namespace Umbraco.Cms.Core.Models.Mapping
Config = new Dictionary<string, object>
{
{ "IsRequired", true }
},
},
// These properties used to live on the member as property data, defaulting to sensitive, so we set them to sensitive here too
new()
{
Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}failedPasswordAttempts",
Label = _localizedTextService.Localize("user", "failedPasswordAttempts"),
Value = member.FailedPasswordAttempts,
View = "readonlyvalue",
IsSensitive = true,
},
new()
{
Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}approved",
Label = _localizedTextService.Localize("user", "stateApproved"),
Value = member.IsApproved,
View = "boolean",
IsSensitive = true,
Readonly = false,
},
new()
{
Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}lockedOut",
Label = _localizedTextService.Localize("user", "stateLockedOut"),
Value = member.IsLockedOut,
View = "boolean",
IsSensitive = true,
Readonly = !member.IsLockedOut, // IMember.IsLockedOut can't be set to true, so make it readonly when that's the case (you can only unlock)
},
new()
{
Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}lastLockoutDate",
Label = _localizedTextService.Localize("user", "lastLockoutDate"),
Value = member.LastLockoutDate?.ToString(),
View = "readonlyvalue",
IsSensitive = true,
},
new()
{
Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}lastLoginDate",
Label = _localizedTextService.Localize("user", "lastLogin"),
Value = member.LastLoginDate?.ToString(),
View = "readonlyvalue",
IsSensitive = true,
},
new()
{
Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}lastPasswordChangeDate",
Label = _localizedTextService.Localize("user", "lastPasswordChangeDate"),
Value = member.LastPasswordChangeDate?.ToString(),
View = "readonlyvalue",
IsSensitive = true,
},
};
if (_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.HasAccessToSensitiveData() is false)
{
// Current user doesn't have access to sensitive data so explicitly set the views and remove the value from sensitive data
foreach (var property in properties)
{
if (property.IsSensitive)
{
property.Value = null;
property.View = "sensitivevalue";
property.Readonly = true;
}
}
};
}
return properties;
}

View File

@@ -294,7 +294,7 @@ namespace Umbraco.Cms.Core.Models.Mapping
target.Id = source.Id;
target.Key = source.Key;
target.LastLockoutDate = source.LastLockoutDate;
target.LastLoginDate = source.LastLoginDate == default ? null : (DateTime?)source.LastLoginDate;
target.LastLoginDate = source.LastLoginDate == default(DateTime) ? null : (DateTime?)source.LastLoginDate;
target.LastPasswordChangeDate = source.LastPasswordChangeDate;
target.Name = source.Name;
target.Navigation = CreateUserEditorNavigation();

View File

@@ -21,6 +21,12 @@ namespace Umbraco.Cms.Core.Models
private string _passwordConfig;
private DateTime? _emailConfirmedDate;
private string _securityStamp;
private int _failedPasswordAttempts;
private bool _isApproved;
private bool _isLockedOut;
private DateTime? _lastLockoutDate;
private DateTime? _lastLoginDate;
private DateTime? _lastPasswordChangeDate;
/// <summary>
/// Initializes a new instance of the <see cref="Member"/> class.
@@ -281,41 +287,13 @@ namespace Umbraco.Cms.Core.Models
}
/// <summary>
/// Gets or sets a boolean indicating whether the Member is approved
/// Gets or sets a value indicating whether the Member is approved
/// </summary>
/// <remarks>
/// Alias: umbracoMemberApproved
/// Part of the standard properties collection.
/// </remarks>
[DataMember]
public bool IsApproved
{
get
{
var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.IsApproved, nameof(IsApproved),
//This is the default value if the prop is not found
true);
if (a.Success == false)
return a.Result;
if (Properties[Constants.Conventions.Member.IsApproved].GetValue() == null)
return true;
var tryConvert = Properties[Constants.Conventions.Member.IsApproved].GetValue().TryConvertTo<bool>();
if (tryConvert.Success)
{
return tryConvert.Result;
}
//if the property exists but it cannot be converted, we will assume true
return true;
}
set
{
if (WarnIfPropertyTypeNotFoundOnSet(
Constants.Conventions.Member.IsApproved,
nameof(IsApproved)) == false)
return;
Properties[Constants.Conventions.Member.IsApproved].SetValue(value);
}
get => _isApproved;
set => SetPropertyValueAndDetectChanges(value, ref _isApproved, nameof(IsApproved));
}
/// <summary>
@@ -328,30 +306,8 @@ namespace Umbraco.Cms.Core.Models
[DataMember]
public bool IsLockedOut
{
get
{
var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.IsLockedOut, nameof(IsLockedOut), false);
if (a.Success == false)
return a.Result;
if (Properties[Constants.Conventions.Member.IsLockedOut].GetValue() == null)
return false;
var tryConvert = Properties[Constants.Conventions.Member.IsLockedOut].GetValue().TryConvertTo<bool>();
if (tryConvert.Success)
{
return tryConvert.Result;
}
return false;
// TODO: Use TryConvertTo<T> instead
}
set
{
if (WarnIfPropertyTypeNotFoundOnSet(
Constants.Conventions.Member.IsLockedOut,
nameof(IsLockedOut)) == false)
return;
Properties[Constants.Conventions.Member.IsLockedOut].SetValue(value);
}
get => _isLockedOut;
set => SetPropertyValueAndDetectChanges(value, ref _isLockedOut, nameof(IsLockedOut));
}
/// <summary>
@@ -362,32 +318,10 @@ namespace Umbraco.Cms.Core.Models
/// Part of the standard properties collection.
/// </remarks>
[DataMember]
public DateTime LastLoginDate
public DateTime? LastLoginDate
{
get
{
var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.LastLoginDate, nameof(LastLoginDate), default(DateTime));
if (a.Success == false)
return a.Result;
if (Properties[Constants.Conventions.Member.LastLoginDate].GetValue() == null)
return default(DateTime);
var tryConvert = Properties[Constants.Conventions.Member.LastLoginDate].GetValue().TryConvertTo<DateTime>();
if (tryConvert.Success)
{
return tryConvert.Result;
}
return default(DateTime);
// TODO: Use TryConvertTo<T> instead
}
set
{
if (WarnIfPropertyTypeNotFoundOnSet(
Constants.Conventions.Member.LastLoginDate,
nameof(LastLoginDate)) == false)
return;
Properties[Constants.Conventions.Member.LastLoginDate].SetValue(value);
}
get => _lastLoginDate;
set => SetPropertyValueAndDetectChanges(value, ref _lastLoginDate, nameof(LastLoginDate));
}
/// <summary>
@@ -398,32 +332,10 @@ namespace Umbraco.Cms.Core.Models
/// Part of the standard properties collection.
/// </remarks>
[DataMember]
public DateTime LastPasswordChangeDate
public DateTime? LastPasswordChangeDate
{
get
{
var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.LastPasswordChangeDate, nameof(LastPasswordChangeDate), default(DateTime));
if (a.Success == false)
return a.Result;
if (Properties[Constants.Conventions.Member.LastPasswordChangeDate].GetValue() == null)
return default(DateTime);
var tryConvert = Properties[Constants.Conventions.Member.LastPasswordChangeDate].GetValue().TryConvertTo<DateTime>();
if (tryConvert.Success)
{
return tryConvert.Result;
}
return default(DateTime);
// TODO: Use TryConvertTo<T> instead
}
set
{
if (WarnIfPropertyTypeNotFoundOnSet(
Constants.Conventions.Member.LastPasswordChangeDate,
nameof(LastPasswordChangeDate)) == false)
return;
Properties[Constants.Conventions.Member.LastPasswordChangeDate].SetValue(value);
}
get => _lastPasswordChangeDate;
set => SetPropertyValueAndDetectChanges(value, ref _lastPasswordChangeDate, nameof(LastPasswordChangeDate));
}
/// <summary>
@@ -434,32 +346,10 @@ namespace Umbraco.Cms.Core.Models
/// Part of the standard properties collection.
/// </remarks>
[DataMember]
public DateTime LastLockoutDate
public DateTime? LastLockoutDate
{
get
{
var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.LastLockoutDate, nameof(LastLockoutDate), default(DateTime));
if (a.Success == false)
return a.Result;
if (Properties[Constants.Conventions.Member.LastLockoutDate].GetValue() == null)
return default(DateTime);
var tryConvert = Properties[Constants.Conventions.Member.LastLockoutDate].GetValue().TryConvertTo<DateTime>();
if (tryConvert.Success)
{
return tryConvert.Result;
}
return default(DateTime);
// TODO: Use TryConvertTo<T> instead
}
set
{
if (WarnIfPropertyTypeNotFoundOnSet(
Constants.Conventions.Member.LastLockoutDate,
nameof(LastLockoutDate)) == false)
return;
Properties[Constants.Conventions.Member.LastLockoutDate].SetValue(value);
}
get => _lastLockoutDate;
set => SetPropertyValueAndDetectChanges(value, ref _lastLockoutDate, nameof(LastLockoutDate));
}
/// <summary>
@@ -473,30 +363,8 @@ namespace Umbraco.Cms.Core.Models
[DataMember]
public int FailedPasswordAttempts
{
get
{
var a = WarnIfPropertyTypeNotFoundOnGet(Constants.Conventions.Member.FailedPasswordAttempts, nameof(FailedPasswordAttempts), 0);
if (a.Success == false)
return a.Result;
if (Properties[Constants.Conventions.Member.FailedPasswordAttempts].GetValue() == null)
return default(int);
var tryConvert = Properties[Constants.Conventions.Member.FailedPasswordAttempts].GetValue().TryConvertTo<int>();
if (tryConvert.Success)
{
return tryConvert.Result;
}
return default(int);
// TODO: Use TryConvertTo<T> instead
}
set
{
if (WarnIfPropertyTypeNotFoundOnSet(
Constants.Conventions.Member.FailedPasswordAttempts,
nameof(FailedPasswordAttempts)) == false)
return;
Properties[Constants.Conventions.Member.FailedPasswordAttempts].SetValue(value);
}
get => _failedPasswordAttempts;
set => SetPropertyValueAndDetectChanges(value, ref _failedPasswordAttempts, nameof(FailedPasswordAttempts));
}
/// <summary>

View File

@@ -25,9 +25,9 @@ namespace Umbraco.Cms.Core.Models.Membership
string Comments { get; set; }
bool IsApproved { get; set; }
bool IsLockedOut { get; set; }
DateTime LastLoginDate { get; set; }
DateTime LastPasswordChangeDate { get; set; }
DateTime LastLockoutDate { get; set; }
DateTime? LastLoginDate { get; set; }
DateTime? LastPasswordChangeDate { get; set; }
DateTime? LastLockoutDate { get; set; }
/// <summary>
/// Gets or sets the number of failed password attempts.

View File

@@ -116,9 +116,9 @@ namespace Umbraco.Cms.Core.Models.Membership
private bool _isApproved;
private bool _isLockedOut;
private string _language;
private DateTime _lastPasswordChangedDate;
private DateTime _lastLoginDate;
private DateTime _lastLockoutDate;
private DateTime? _lastPasswordChangedDate;
private DateTime? _lastLoginDate;
private DateTime? _lastLockoutDate;
//Custom comparer for enumerable
private static readonly DelegateEqualityComparer<IEnumerable<int>> IntegerEnumerableComparer =
@@ -184,21 +184,21 @@ namespace Umbraco.Cms.Core.Models.Membership
}
[IgnoreDataMember]
public DateTime LastLoginDate
public DateTime? LastLoginDate
{
get => _lastLoginDate;
set => SetPropertyValueAndDetectChanges(value, ref _lastLoginDate, nameof(LastLoginDate));
}
[IgnoreDataMember]
public DateTime LastPasswordChangeDate
public DateTime? LastPasswordChangeDate
{
get => _lastPasswordChangedDate;
set => SetPropertyValueAndDetectChanges(value, ref _lastPasswordChangedDate, nameof(LastPasswordChangeDate));
}
[IgnoreDataMember]
public DateTime LastLockoutDate
public DateTime? LastLockoutDate
{
get => _lastLockoutDate;
set => SetPropertyValueAndDetectChanges(value, ref _lastLockoutDate, nameof(LastLockoutDate));

View File

@@ -51,6 +51,7 @@ namespace Umbraco.Cms.Core.Persistence.Repositories
/// updating their login date. This operation must be fast and cannot use database locks which is fine if we are only executing a single query
/// for this data since there won't be any other data contention issues.
/// </remarks>
[Obsolete("This is now a NoOp since last login date is no longer an umbraco property, set the date on the IMember directly and Save it instead, scheduled for removal in V11.")]
void SetLastLogin(string username, DateTime date);
}
}

View File

@@ -67,10 +67,10 @@ namespace Umbraco.Cms.Core.Services
query = Query<IMember>();
break;
case MemberCountType.LockedOut:
query = Query<IMember>().Where(x => x.PropertyTypeAlias == Constants.Conventions.Member.IsLockedOut && ((Member) x).BoolPropertyValue);
query = Query<IMember>().Where(x => x.IsLockedOut == true);
break;
case MemberCountType.Approved:
query = Query<IMember>().Where(x => x.PropertyTypeAlias == Constants.Conventions.Member.IsApproved && ((Member) x).BoolPropertyValue);
query = Query<IMember>().Where(x => x.IsApproved == true);
break;
default:
throw new ArgumentOutOfRangeException(nameof(countType));
@@ -748,13 +748,9 @@ namespace Umbraco.Cms.Core.Services
#region Save
/// <inheritdoc />
[Obsolete("This is now a NoOp since last login date is no longer an umbraco property, set the date on the IMember directly and Save it instead, scheduled for removal in V11.")]
public void SetLastLogin(string username, DateTime date)
{
using (var scope = ScopeProvider.CreateScope())
{
_memberRepository.SetLastLogin(username, date);
scope.Complete();
}
}
/// <inheritdoc />

View File

@@ -638,13 +638,15 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install
// Membership property types.
if (_database.Exists<PropertyTypeGroupDto>(11))
{
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 28, UniqueId = 28.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.Textarea, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Cms.Core.Constants.Conventions.Member.Comments, Name = Cms.Core.Constants.Conventions.Member.CommentsLabel, SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing });
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 29, UniqueId = 29.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.LabelInt, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Cms.Core.Constants.Conventions.Member.FailedPasswordAttempts, Name = Cms.Core.Constants.Conventions.Member.FailedPasswordAttemptsLabel, SortOrder = 1, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing });
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 30, UniqueId = 30.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.Boolean, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Cms.Core.Constants.Conventions.Member.IsApproved, Name = Cms.Core.Constants.Conventions.Member.IsApprovedLabel, SortOrder = 2, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing });
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 31, UniqueId = 31.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.Boolean, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Cms.Core.Constants.Conventions.Member.IsLockedOut, Name = Cms.Core.Constants.Conventions.Member.IsLockedOutLabel, SortOrder = 3, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing });
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 32, UniqueId = 32.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Cms.Core.Constants.Conventions.Member.LastLockoutDate, Name = Cms.Core.Constants.Conventions.Member.LastLockoutDateLabel, SortOrder = 4, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing });
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 33, UniqueId = 33.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Cms.Core.Constants.Conventions.Member.LastLoginDate, Name = Cms.Core.Constants.Conventions.Member.LastLoginDateLabel, SortOrder = 5, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing });
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 34, UniqueId = 34.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Cms.Core.Constants.Conventions.Member.LastPasswordChangeDate, Name = Cms.Core.Constants.Conventions.Member.LastPasswordChangeDateLabel, SortOrder = 6, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing });
_database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyType, "id", false,
new PropertyTypeDto
{
Id = 28, UniqueId = 28.ToGuid(), DataTypeId = Cms.Core.Constants.DataTypes.Textarea,
ContentTypeId = 1044, PropertyTypeGroupId = 11,
Alias = Cms.Core.Constants.Conventions.Member.Comments,
Name = Cms.Core.Constants.Conventions.Member.CommentsLabel, SortOrder = 0, Mandatory = false,
ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing
});
}
}

View File

@@ -3,6 +3,7 @@ using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Semver;
using Umbraco.Cms.Infrastructure.Migrations.Upgrade.Common;
using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_10_0_0;
using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0;
using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_1;
using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_1_0;
@@ -284,6 +285,9 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade
// TO 9.4.0
To<AddScheduledPublishingLock>("{DBBA1EA0-25A1-4863-90FB-5D306FB6F1E1}");
To<UpdateRelationTypesToHandleDependencies>("{DED98755-4059-41BB-ADBD-3FEAB12D1D7B}");
// TO 10.0.0
To<AddMemberPropertiesAsColumns>("{B7E0D53C-2B0E-418B-AB07-2DDE486E225F}");
}
}
}

View File

@@ -0,0 +1,242 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NPoco;
using Umbraco.Cms.Core;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_10_0_0;
public class AddMemberPropertiesAsColumns : MigrationBase
{
public AddMemberPropertiesAsColumns(IMigrationContext context)
: base(context)
{
}
protected override void Migrate()
{
var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList();
AddColumnIfNotExists<MemberDto>(columns, "failedPasswordAttempts");
AddColumnIfNotExists<MemberDto>(columns, "isLockedOut");
AddColumnIfNotExists<MemberDto>(columns, "isApproved");
AddColumnIfNotExists<MemberDto>(columns, "lastLoginDate");
AddColumnIfNotExists<MemberDto>(columns, "lastLockoutDate");
AddColumnIfNotExists<MemberDto>(columns, "lastPasswordChangeDate");
Sql<ISqlContext> newestContentVersionQuery = Database.SqlContext.Sql()
.Select($"MAX({GetQuotedSelector("cv", "id")}) as {SqlSyntax.GetQuotedColumnName("id")}", GetQuotedSelector("cv", "nodeId"))
.From<ContentVersionDto>("cv")
.GroupBy(GetQuotedSelector("cv", "nodeId"));
Sql<ISqlContext> passwordAttemptsQuery = Database.SqlContext.Sql()
.Select(GetSubQueryColumns())
.From<PropertyTypeDto>("pt")
.Where($"{GetQuotedSelector("pt", "Alias")} = 'umbracoMemberFailedPasswordAttempts'");
Sql<ISqlContext> memberApprovedQuery = Database.SqlContext.Sql()
.Select(GetSubQueryColumns())
.From<PropertyTypeDto>("pt")
.Where($"{GetQuotedSelector("pt", "Alias")} = 'umbracoMemberApproved'");
Sql<ISqlContext> memberLockedOutQuery = Database.SqlContext.Sql()
.Select(GetSubQueryColumns())
.From<PropertyTypeDto>("pt")
.Where($"{GetQuotedSelector("pt", "Alias")} = 'umbracoMemberLockedOut'");
Sql<ISqlContext> memberLastLockoutDateQuery = Database.SqlContext.Sql()
.Select(GetSubQueryColumns())
.From<PropertyTypeDto>("pt")
.Where($"{GetQuotedSelector("pt", "Alias")} = 'umbracoMemberLastLockoutDate'");
Sql<ISqlContext> memberLastLoginDateQuery = Database.SqlContext.Sql()
.Select(GetSubQueryColumns())
.From<PropertyTypeDto>("pt")
.Where($"{GetQuotedSelector("pt", "Alias")} = 'umbracoMemberLastLogin'");
Sql<ISqlContext> memberLastPasswordChangeDateQuery = Database.SqlContext.Sql()
.Select(GetSubQueryColumns())
.From<PropertyTypeDto>("pt")
.Where($"{GetQuotedSelector("pt", "Alias")} = 'umbracoMemberLastPasswordChangeDate'");
StringBuilder queryBuilder = new StringBuilder();
queryBuilder.AppendLine($"UPDATE {Constants.DatabaseSchema.Tables.Member}");
queryBuilder.AppendLine("SET");
queryBuilder.AppendLine($"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate<MemberDto>(x => x.FailedPasswordAttempts)} = {GetQuotedSelector("umbracoPropertyData", "intValue")},");
queryBuilder.AppendLine($"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate<MemberDto>(x => x.IsApproved)} = {GetQuotedSelector("pdmp", "intValue")},");
queryBuilder.AppendLine($"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate<MemberDto>(x => x.IsLockedOut)} = {GetQuotedSelector("pdlo", "intValue")},");
queryBuilder.AppendLine($"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate<MemberDto>(x => x.LastLockoutDate)} = {GetQuotedSelector("pdlout", "dateValue")},");
queryBuilder.AppendLine($"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate<MemberDto>(x => x.LastLoginDate)} = {GetQuotedSelector("pdlin", "dateValue")},");
queryBuilder.Append($"\t{Database.SqlContext.SqlSyntax.GetFieldNameForUpdate<MemberDto>(x => x.LastPasswordChangeDate)} = {GetQuotedSelector("pdlpc", "dateValue")}");
Sql<ISqlContext> updateMemberColumnsQuery = Database.SqlContext.Sql(queryBuilder.ToString())
.From<NodeDto>()
.InnerJoin<ContentDto>()
.On<NodeDto, ContentDto>((left, right) => left.NodeId == right.NodeId)
.InnerJoin<ContentTypeDto>()
.On<ContentDto, ContentTypeDto>((left, right) => left.ContentTypeId == right.NodeId)
.InnerJoin(newestContentVersionQuery, "umbracoContentVersion")
.On<NodeDto, ContentVersionDto>((left, right) => left.NodeId == right.NodeId)
.InnerJoin<MemberDto>("m")
.On<ContentDto, MemberDto>((left, right) => left.NodeId == right.NodeId, null, "m")
.LeftJoin(passwordAttemptsQuery, "failedAttemptsType")
.On<ContentDto, FailedAttempts>((left, right) => left.ContentTypeId == right.ContentTypeId)
.LeftJoin<DataTypeDto>()
.On<FailedAttempts, DataTypeDto>((left, right) => left.DataTypeId == right.NodeId)
.LeftJoin<PropertyDataDto>()
.On<PropertyDataDto, FailedAttempts, ContentVersionDto>((left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id)
.LeftJoin(memberApprovedQuery, "memberApprovedType")
.On<ContentDto, MemberApproved>((left, right) => left.ContentTypeId == right.ContentTypeId)
.LeftJoin<DataTypeDto>("dtmp")
.On<MemberApproved, DataTypeDto>((left, right) => left.DataTypeId == right.NodeId, null, "dtmp")
.LeftJoin<PropertyDataDto>("pdmp")
.On<PropertyDataDto, MemberApproved, ContentVersionDto>((left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdmp")
.LeftJoin(memberLockedOutQuery, "memberLockedOutType")
.On<ContentDto, MemberLockedOut>((left, right) => left.ContentTypeId == right.ContentTypeId)
.LeftJoin<DataTypeDto>("dtlo")
.On<MemberLockedOut, DataTypeDto>((left, right) => left.DataTypeId == right.NodeId, null, "dtlo")
.LeftJoin<PropertyDataDto>("pdlo")
.On<PropertyDataDto, MemberLockedOut, ContentVersionDto>((left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdlo")
.LeftJoin(memberLastLockoutDateQuery, "lastLockOutDateType")
.On<ContentDto, LastLockoutDate>((left, right) => left.ContentTypeId == right.ContentTypeId)
.LeftJoin<DataTypeDto>("dtlout")
.On<LastLockoutDate, DataTypeDto>((left, right) => left.DataTypeId == right.NodeId, null, "dtlout")
.LeftJoin<PropertyDataDto>("pdlout")
.On<PropertyDataDto, LastLockoutDate, ContentVersionDto>((left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdlout")
.LeftJoin(memberLastLoginDateQuery, "lastLoginDateType")
.On<ContentDto, LastLoginDate>((left, right) => left.ContentTypeId == right.ContentTypeId)
.LeftJoin<DataTypeDto>("dtlin")
.On<LastLoginDate, DataTypeDto>((left, right) => left.DataTypeId == right.NodeId, null, "dtlin")
.LeftJoin<PropertyDataDto>("pdlin")
.On<PropertyDataDto, LastLoginDate, ContentVersionDto>((left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdlin")
.LeftJoin(memberLastPasswordChangeDateQuery, "lastPasswordChangeType")
.On<ContentDto, LastPasswordChange>((left, right) => left.ContentTypeId == right.ContentTypeId)
.LeftJoin<DataTypeDto>("dtlpc")
.On<LastPasswordChange, DataTypeDto>((left, right) => left.DataTypeId == right.NodeId, null, "dtlpc")
.LeftJoin<PropertyDataDto>("pdlpc")
.On<PropertyDataDto, LastPasswordChange, ContentVersionDto>((left, middle, right) => left.PropertyTypeId == middle.Id && left.VersionId == right.Id, "pdlpc")
.Where<NodeDto>(x => x.NodeObjectType == Constants.ObjectTypes.Member);
Database.Execute(updateMemberColumnsQuery);
// Removing old property types and values, since these are no longer needed
// Hard coding the aliases, since we want to be able to delete the constants...
string[] propertyTypesToDelete =
{
"umbracoMemberFailedPasswordAttempts",
"umbracoMemberApproved",
"umbracoMemberLockedOut",
"umbracoMemberLastLockoutDate",
"umbracoMemberLastLogin",
"umbracoMemberLastPasswordChangeDate"
};
Sql<ISqlContext> idQuery = Database.SqlContext.Sql().Select<PropertyTypeDto>(x => x.Id)
.From<PropertyTypeDto>()
.WhereIn<PropertyTypeDto>(x => x.Alias, propertyTypesToDelete);
List<int> idsToDelete = Database.Fetch<int>(idQuery);
// Firstly deleting the property data due to FK constraints
Sql<ISqlContext> propertyDataDelete = Database.SqlContext.Sql()
.Delete<PropertyDataDto>()
.WhereIn<PropertyDataDto>(x => x.PropertyTypeId, idsToDelete);
Database.Execute(propertyDataDelete);
// Then we can remove the property
Sql<ISqlContext> propertyTypeDelete = Database.SqlContext.Sql()
.Delete<PropertyTypeDto>()
.WhereIn<PropertyTypeDto>(x => x.Id, idsToDelete);
Database.Execute(propertyTypeDelete);
}
private string GetQuotedSelector(string tableName, string columnName)
=> $"{SqlSyntax.GetQuotedTableName(tableName)}.{SqlSyntax.GetQuotedColumnName(columnName)}";
private object[] GetSubQueryColumns() => new object[]
{
SqlSyntax.GetQuotedColumnName("contentTypeId"),
SqlSyntax.GetQuotedColumnName("dataTypeId"),
SqlSyntax.GetQuotedColumnName("id"),
};
[TableName("failedAttemptsType")]
private class FailedAttempts
{
[Column("contentTypeId")]
public int ContentTypeId { get; set; }
[Column("dataTypeId")]
public int DataTypeId { get; set; }
[Column("id")]
public int Id { get; set; }
}
[TableName("memberApprovedType")]
private class MemberApproved
{
[Column("contentTypeId")]
public int ContentTypeId { get; set; }
[Column("dataTypeId")]
public int DataTypeId { get; set; }
[Column("id")]
public int Id { get; set; }
}
[TableName("memberLockedOutType")]
private class MemberLockedOut
{
[Column("contentTypeId")]
public int ContentTypeId { get; set; }
[Column("dataTypeId")]
public int DataTypeId { get; set; }
[Column("id")]
public int Id { get; set; }
}
[TableName("lastLockOutDateType")]
private class LastLockoutDate
{
[Column("contentTypeId")]
public int ContentTypeId { get; set; }
[Column("dataTypeId")]
public int DataTypeId { get; set; }
[Column("id")]
public int Id { get; set; }
}
[TableName("lastLoginDateType")]
private class LastLoginDate
{
[Column("contentTypeId")]
public int ContentTypeId { get; set; }
[Column("dataTypeId")]
public int DataTypeId { get; set; }
[Column("id")]
public int Id { get; set; }
}
[TableName("lastPasswordChangeType")]
private class LastPasswordChange
{
[Column("contentTypeId")]
public int ContentTypeId { get; set; }
[Column("dataTypeId")]
public int DataTypeId { get; set; }
[Column("id")]
public int Id { get; set; }
}
}

View File

@@ -49,7 +49,30 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Dtos
[NullSetting(NullSetting = NullSettings.Null)]
public DateTime? EmailConfirmedDate { get; set; }
// TODO: It would be SOOOOO much better to store all core member data here instead of hiding it in Umbraco properties
[Column("failedPasswordAttempts")]
[NullSetting(NullSetting = NullSettings.Null)]
public int? FailedPasswordAttempts { get; set; }
[Column("isLockedOut")]
[Constraint(Default = 0)]
[NullSetting(NullSetting = NullSettings.Null)]
public bool IsLockedOut { get; set; }
[Column("isApproved")]
[Constraint(Default = 1)]
public bool IsApproved { get; set; }
[Column("lastLoginDate")]
[NullSetting(NullSetting = NullSettings.Null)]
public DateTime? LastLoginDate { get; set; }
[Column("lastLockoutDate")]
[NullSetting(NullSetting = NullSettings.Null)]
public DateTime? LastLockoutDate { get; set; }
[Column("lastPasswordChangeDate")]
[NullSetting(NullSetting = NullSettings.Null)]
public DateTime? LastPasswordChangeDate { get; set; }
[ResultColumn]
[Reference(ReferenceType.OneToOne, ReferenceMemberName = "NodeId")]

View File

@@ -130,7 +130,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories
content.PasswordConfiguration = dto.PasswordConfig;
content.Key = nodeDto.UniqueId;
content.VersionId = contentVersionDto.Id;
// TODO: missing names?
content.Path = nodeDto.Path;
@@ -143,6 +143,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories
content.WriterId = contentVersionDto.UserId ?? Cms.Core.Constants.Security.UnknownUserId;
content.CreateDate = nodeDto.CreateDate;
content.UpdateDate = contentVersionDto.VersionDate;
content.FailedPasswordAttempts = dto.FailedPasswordAttempts ?? default;
content.IsLockedOut = dto.IsLockedOut;
content.IsApproved = dto.IsApproved;
content.LastLoginDate = dto.LastLoginDate;
content.LastLockoutDate = dto.LastLockoutDate;
content.LastPasswordChangeDate = dto.LastPasswordChangeDate;
// reset dirty initial properties (U4-1946)
content.ResetDirtyProperties(false);
@@ -219,7 +225,13 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories
EmailConfirmedDate = entity.EmailConfirmedDate,
ContentDto = contentDto,
ContentVersionDto = BuildContentVersionDto(entity, contentDto),
PasswordConfig = entity.PasswordConfiguration
PasswordConfig = entity.PasswordConfiguration,
FailedPasswordAttempts = entity.FailedPasswordAttempts,
IsApproved = entity.IsApproved,
IsLockedOut = entity.IsLockedOut,
LastLockoutDate = entity.LastLockoutDate,
LastLoginDate = entity.LastLoginDate,
LastPasswordChangeDate = entity.LastPasswordChangeDate,
};
return dto;
}

View File

@@ -28,9 +28,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories
user.Language = dto.UserLanguage;
user.SecurityStamp = dto.SecurityStampToken;
user.FailedPasswordAttempts = dto.FailedLoginAttempts ?? 0;
user.LastLockoutDate = dto.LastLockoutDate ?? DateTime.MinValue;
user.LastLoginDate = dto.LastLoginDate ?? DateTime.MinValue;
user.LastPasswordChangeDate = dto.LastPasswordChangeDate ?? DateTime.MinValue;
user.LastLockoutDate = dto.LastLockoutDate;
user.LastLoginDate = dto.LastLoginDate;
user.LastPasswordChangeDate = dto.LastPasswordChangeDate;
user.CreateDate = dto.CreateDate;
user.UpdateDate = dto.UpdateDate;
user.Avatar = dto.Avatar;

View File

@@ -35,14 +35,14 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Mappers
DefineMap<Member, MemberDto>(nameof(Member.Email), nameof(MemberDto.Email));
DefineMap<Member, MemberDto>(nameof(Member.Username), nameof(MemberDto.LoginName));
DefineMap<Member, MemberDto>(nameof(Member.RawPasswordValue), nameof(MemberDto.Password));
DefineMap<Member, MemberDto>(nameof(Member.IsApproved), nameof(MemberDto.IsApproved));
DefineMap<Member, MemberDto>(nameof(Member.IsLockedOut), nameof(MemberDto.IsLockedOut));
DefineMap<Member, MemberDto>(nameof(Member.FailedPasswordAttempts), nameof(MemberDto.FailedPasswordAttempts));
DefineMap<Member, MemberDto>(nameof(Member.LastLockoutDate), nameof(MemberDto.LastLockoutDate));
DefineMap<Member, MemberDto>(nameof(Member.LastLoginDate), nameof(MemberDto.LastLoginDate));
DefineMap<Member, MemberDto>(nameof(Member.LastPasswordChangeDate), nameof(MemberDto.LastPasswordChangeDate));
DefineMap<Member, PropertyDataDto>(nameof(Member.IsApproved), nameof(PropertyDataDto.IntegerValue));
DefineMap<Member, PropertyDataDto>(nameof(Member.IsLockedOut), nameof(PropertyDataDto.IntegerValue));
DefineMap<Member, PropertyDataDto>(nameof(Member.Comments), nameof(PropertyDataDto.TextValue));
DefineMap<Member, PropertyDataDto>(nameof(Member.FailedPasswordAttempts), nameof(PropertyDataDto.IntegerValue));
DefineMap<Member, PropertyDataDto>(nameof(Member.LastLockoutDate), nameof(PropertyDataDto.DateValue));
DefineMap<Member, PropertyDataDto>(nameof(Member.LastLoginDate), nameof(PropertyDataDto.DateValue));
DefineMap<Member, PropertyDataDto>(nameof(Member.LastPasswordChangeDate), nameof(PropertyDataDto.DateValue));
/* Internal experiment */
DefineMap<Member, PropertyDataDto>(nameof(Member.DateTimePropertyValue), nameof(PropertyDataDto.DateValue));

View File

@@ -405,6 +405,24 @@ namespace Umbraco.Extensions
return sql.InnerJoin(join);
}
/// <summary>
/// Appends an INNER JOIN clause using a nested query.
/// </summary>
/// <param name="sql">The SQL statement.</param>
/// <param name="nestedSelect">The nested sql query.</param>
/// <param name="alias">An optional alias for the joined table.</param>
/// <returns>A SqlJoin statement.</returns>
public static Sql<ISqlContext>.SqlJoinClause<ISqlContext> InnerJoin(this Sql<ISqlContext> sql, Sql<ISqlContext> nestedSelect, string alias = null)
{
var join = $"({nestedSelect.SQL})";
if (alias is not null)
{
join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias);
}
return sql.InnerJoin(join);
}
/// <summary>
/// Appends a LEFT JOIN clause to the Sql statement.
/// </summary>
@@ -437,6 +455,24 @@ namespace Umbraco.Extensions
string alias = null) =>
sql.SqlContext.SqlSyntax.LeftJoinWithNestedJoin<TDto>(sql, nestedJoin, alias);
/// <summary>
/// Appends an LEFT JOIN clause using a nested query.
/// </summary>
/// <param name="sql">The SQL statement.</param>
/// <param name="nestedSelect">The nested sql query.</param>
/// <param name="alias">An optional alias for the joined table.</param>
/// <returns>A SqlJoin statement.</returns>
public static Sql<ISqlContext>.SqlJoinClause<ISqlContext> LeftJoin(this Sql<ISqlContext> sql, Sql<ISqlContext> nestedSelect, string alias = null)
{
var join = $"({nestedSelect.SQL})";
if (alias is not null)
{
join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias);
}
return sql.LeftJoin(join);
}
/// <summary>
/// Appends a RIGHT JOIN clause to the Sql statement.
/// </summary>

View File

@@ -207,58 +207,11 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
}
/// <inheritdoc />
[Obsolete(
"This is now a NoOp since last login date is no longer an umbraco property, set the date on the IMember directly and Save it instead, scheduled for removal in V11.")]
public void SetLastLogin(string username, DateTime date)
{
// Important - these queries are designed to execute without an exclusive WriteLock taken in our distributed lock
// table. However due to the data that we are updating which relies on version data we cannot update this data
// without taking some locks, otherwise we'll end up with strange situations because when a member is updated, that operation
// deletes and re-inserts all property data. So if there are concurrent transactions, one deleting and re-inserting and another trying
// to update there can be problems. This is only an issue for cmsPropertyData, not umbracoContentVersion because that table just
// maintains a single row and it isn't deleted/re-inserted.
// So the important part here is the ForUpdate() call on the select to fetch the property data to update.
// Update the cms property value for the member
SqlTemplate sqlSelectTemplateProperty = SqlContext.Templates.Get(
"Umbraco.Core.MemberRepository.SetLastLogin1", s => s
.Select<PropertyDataDto>(x => x.Id)
.From<PropertyDataDto>()
.InnerJoin<PropertyTypeDto>()
.On<PropertyTypeDto, PropertyDataDto>((l, r) => l.Id == r.PropertyTypeId)
.InnerJoin<ContentVersionDto>()
.On<ContentVersionDto, PropertyDataDto>((l, r) => l.Id == r.VersionId)
.InnerJoin<NodeDto>().On<NodeDto, ContentVersionDto>((l, r) => l.NodeId == r.NodeId)
.InnerJoin<MemberDto>().On<MemberDto, NodeDto>((l, r) => l.NodeId == r.NodeId)
.Where<NodeDto>(x => x.NodeObjectType == SqlTemplate.Arg<Guid>("nodeObjectType"))
.Where<PropertyTypeDto>(x => x.Alias == SqlTemplate.Arg<string>("propertyTypeAlias"))
.Where<MemberDto>(x => x.LoginName == SqlTemplate.Arg<string>("username"))
.ForUpdate());
Sql<ISqlContext> sqlSelectProperty = sqlSelectTemplateProperty.Sql(Constants.ObjectTypes.Member,
Constants.Conventions.Member.LastLoginDate, username);
Sql<ISqlContext> update = Sql()
.Update<PropertyDataDto>(u => u
.Set(x => x.DateValue, date))
.WhereIn<PropertyDataDto>(x => x.Id, sqlSelectProperty);
Database.Execute(update);
// Update the umbracoContentVersion value for the member
SqlTemplate sqlSelectTemplateVersion = SqlContext.Templates.Get(
"Umbraco.Core.MemberRepository.SetLastLogin2", s => s
.Select<ContentVersionDto>(x => x.Id)
.From<ContentVersionDto>()
.InnerJoin<NodeDto>().On<NodeDto, ContentVersionDto>((l, r) => l.NodeId == r.NodeId)
.InnerJoin<MemberDto>().On<MemberDto, NodeDto>((l, r) => l.NodeId == r.NodeId)
.Where<NodeDto>(x => x.NodeObjectType == SqlTemplate.Arg<Guid>("nodeObjectType"))
.Where<MemberDto>(x => x.LoginName == SqlTemplate.Arg<string>("username")));
Sql<ISqlContext> sqlSelectVersion = sqlSelectTemplateVersion.Sql(Constants.ObjectTypes.Member, username);
Database.Execute(Sql()
.Update<ContentVersionDto>(u => u
.Set(x => x.VersionDate, date))
.WhereIn<ContentVersionDto>(x => x.Id, sqlSelectVersion));
}
/// <summary>
@@ -772,12 +725,43 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
changedCols.Add("LoginName");
}
if (entity.IsPropertyDirty(nameof(entity.FailedPasswordAttempts)))
{
changedCols.Add(nameof(entity.FailedPasswordAttempts));
}
if (entity.IsPropertyDirty(nameof(entity.IsApproved)))
{
changedCols.Add(nameof(entity.IsApproved));
}
if (entity.IsPropertyDirty(nameof(entity.IsLockedOut)))
{
changedCols.Add(nameof(entity.IsLockedOut));
}
if (entity.IsPropertyDirty(nameof(entity.LastLockoutDate)))
{
changedCols.Add(nameof(entity.LastLockoutDate));
}
if (entity.IsPropertyDirty(nameof(entity.LastLoginDate)))
{
changedCols.Add(nameof(entity.LastLoginDate));
}
if (entity.IsPropertyDirty(nameof(entity.LastPasswordChangeDate)))
{
changedCols.Add(nameof(entity.LastPasswordChangeDate));
}
// this can occur from an upgrade
if (memberDto.PasswordConfig.IsNullOrWhiteSpace())
{
memberDto.PasswordConfig = DefaultPasswordConfigJson;
changedCols.Add("passwordConfig");
}else if (memberDto.PasswordConfig == Constants.Security.UnknownPasswordConfigJson)
}
else if (memberDto.PasswordConfig == Constants.Security.UnknownPasswordConfigJson)
{
changedCols.Add("passwordConfig");
}

View File

@@ -496,12 +496,12 @@ namespace Umbraco.Cms.Core.Security
// don't assign anything if nothing has changed as this will trigger the track changes of the model
if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.LastLoginDateUtc))
|| (user.LastLoginDate != default && identityUser.LastLoginDateUtc.HasValue == false)
|| (identityUser.LastLoginDateUtc.HasValue && user.LastLoginDate.ToUniversalTime() != identityUser.LastLoginDateUtc.Value))
|| (identityUser.LastLoginDateUtc.HasValue && user.LastLoginDate?.ToUniversalTime() != identityUser.LastLoginDateUtc.Value))
{
anythingChanged = true;
// if the LastLoginDate is being set to MinValue, don't convert it ToLocalTime
DateTime dt = identityUser.LastLoginDateUtc == DateTime.MinValue ? DateTime.MinValue : identityUser.LastLoginDateUtc.Value.ToLocalTime();
DateTime? dt = identityUser.LastLoginDateUtc?.ToLocalTime();
user.LastLoginDate = dt;
}
@@ -513,11 +513,11 @@ namespace Umbraco.Cms.Core.Security
}
if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.LastPasswordChangeDateUtc))
|| (user.LastPasswordChangeDate != default && identityUser.LastPasswordChangeDateUtc.HasValue == false)
|| (identityUser.LastPasswordChangeDateUtc.HasValue && user.LastPasswordChangeDate.ToUniversalTime() != identityUser.LastPasswordChangeDateUtc.Value))
|| (user.LastPasswordChangeDate.HasValue && user.LastPasswordChangeDate.Value != default && identityUser.LastPasswordChangeDateUtc.HasValue == false)
|| (identityUser.LastPasswordChangeDateUtc.HasValue && user.LastPasswordChangeDate?.ToUniversalTime() != identityUser.LastPasswordChangeDateUtc.Value))
{
anythingChanged = true;
user.LastPasswordChangeDate = identityUser.LastPasswordChangeDateUtc.Value.ToLocalTime();
user.LastPasswordChangeDate = identityUser.LastPasswordChangeDateUtc?.ToLocalTime();
}
if (identityUser.IsPropertyDirty(nameof(BackOfficeIdentityUser.EmailConfirmed))

View File

@@ -72,8 +72,8 @@ namespace Umbraco.Cms.Core.Security
target.CalculatedContentStartNodeIds = source.CalculateContentStartNodeIds(_entityService, _appCaches);
target.Email = source.Email;
target.UserName = source.Username;
target.LastPasswordChangeDateUtc = source.LastPasswordChangeDate.ToUniversalTime();
target.LastLoginDateUtc = source.LastLoginDate.ToUniversalTime();
target.LastPasswordChangeDateUtc = source.LastPasswordChangeDate?.ToUniversalTime();
target.LastLoginDateUtc = source.LastLoginDate?.ToUniversalTime();
target.InviteDateUtc = source.InvitedDate?.ToUniversalTime();
target.EmailConfirmed = source.EmailConfirmedDate.HasValue;
target.Name = source.Name;
@@ -93,8 +93,8 @@ namespace Umbraco.Cms.Core.Security
{
target.Email = source.Email;
target.UserName = source.Username;
target.LastPasswordChangeDateUtc = source.LastPasswordChangeDate.ToUniversalTime();
target.LastLoginDateUtc = source.LastLoginDate.ToUniversalTime();
target.LastPasswordChangeDateUtc = source.LastPasswordChangeDate?.ToUniversalTime();
target.LastLoginDateUtc = source.LastLoginDate?.ToUniversalTime();
target.EmailConfirmed = source.EmailConfirmedDate.HasValue;
target.Name = source.Name;
target.AccessFailedCount = source.FailedPasswordAttempts;
@@ -104,7 +104,7 @@ namespace Umbraco.Cms.Core.Security
target.SecurityStamp = source.SecurityStamp;
target.LockoutEnd = source.IsLockedOut ? DateTime.MaxValue.ToUniversalTime() : (DateTime?)null;
target.Comments = source.Comments;
target.LastLockoutDateUtc = source.LastLockoutDate == DateTime.MinValue ? null : source.LastLockoutDate.ToUniversalTime();
target.LastLockoutDateUtc = source.LastLockoutDate == DateTime.MinValue ? null : source.LastLockoutDate?.ToUniversalTime();
target.CreatedDateUtc = source.CreateDate.ToUniversalTime();
target.Key = source.Key;
target.MemberTypeAlias = source.ContentTypeAlias;

View File

@@ -174,6 +174,7 @@ namespace Umbraco.Cms.Core.Security
//TODO: should this be thrown, or an identity result?
throw new InvalidOperationException("The user id must be an integer to work with the Umbraco");
}
using IScope scope = _scopeProvider.CreateScope(autoComplete: true);
IMember found = _memberService.GetById(asInt);
@@ -183,17 +184,10 @@ namespace Umbraco.Cms.Core.Security
var isLoginsPropertyDirty = user.IsPropertyDirty(nameof(MemberIdentityUser.Logins));
var isTokensPropertyDirty = user.IsPropertyDirty(nameof(MemberIdentityUser.LoginTokens));
MemberDataChangeType memberChangeType = UpdateMemberProperties(found, user);
if (memberChangeType == MemberDataChangeType.FullSave)
if (UpdateMemberProperties(found, user))
{
_memberService.Save(found);
}
else if (memberChangeType == MemberDataChangeType.LoginOnly)
{
// If the member is only logging in, just issue that command without
// any write locks so we are creating a bottleneck.
_memberService.SetLastLogin(found.Username, DateTime.Now);
}
if (isLoginsPropertyDirty)
{
@@ -588,19 +582,16 @@ namespace Umbraco.Cms.Core.Security
return user;
}
private MemberDataChangeType UpdateMemberProperties(IMember member, MemberIdentityUser identityUser)
private bool UpdateMemberProperties(IMember member, MemberIdentityUser identityUser)
{
MemberDataChangeType changeType = MemberDataChangeType.None;
var anythingChanged = false;
// don't assign anything if nothing has changed as this will trigger the track changes of the model
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.LastLoginDateUtc))
|| (member.LastLoginDate != default && identityUser.LastLoginDateUtc.HasValue == false)
|| (identityUser.LastLoginDateUtc.HasValue && member.LastLoginDate.ToUniversalTime() != identityUser.LastLoginDateUtc.Value))
|| (identityUser.LastLoginDateUtc.HasValue && member.LastLoginDate?.ToUniversalTime() != identityUser.LastLoginDateUtc.Value))
{
// If the LastLoginDate is default on the member we have to do a full save.
// This is because the umbraco property data for the member doesn't exist yet in this case
// meaning we can't just update that property data, but have to do a full save to create it
changeType = member.LastLoginDate == default ? MemberDataChangeType.FullSave : MemberDataChangeType.LoginOnly;
anythingChanged = true;
// if the LastLoginDate is being set to MinValue, don't convert it ToLocalTime
DateTime dt = identityUser.LastLoginDateUtc == DateTime.MinValue ? DateTime.MinValue : identityUser.LastLoginDateUtc.Value.ToLocalTime();
@@ -609,16 +600,16 @@ namespace Umbraco.Cms.Core.Security
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.LastPasswordChangeDateUtc))
|| (member.LastPasswordChangeDate != default && identityUser.LastPasswordChangeDateUtc.HasValue == false)
|| (identityUser.LastPasswordChangeDateUtc.HasValue && member.LastPasswordChangeDate.ToUniversalTime() != identityUser.LastPasswordChangeDateUtc.Value))
|| (identityUser.LastPasswordChangeDateUtc.HasValue && member.LastPasswordChangeDate?.ToUniversalTime() != identityUser.LastPasswordChangeDateUtc.Value))
{
changeType = MemberDataChangeType.FullSave;
anythingChanged = true;
member.LastPasswordChangeDate = identityUser.LastPasswordChangeDateUtc.Value.ToLocalTime();
}
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.Comments))
&& member.Comments != identityUser.Comments && identityUser.Comments.IsNullOrWhiteSpace() == false)
{
changeType = MemberDataChangeType.FullSave;
anythingChanged = true;
member.Comments = identityUser.Comments;
}
@@ -626,34 +617,34 @@ namespace Umbraco.Cms.Core.Security
|| (member.EmailConfirmedDate.HasValue && member.EmailConfirmedDate.Value != default && identityUser.EmailConfirmed == false)
|| ((member.EmailConfirmedDate.HasValue == false || member.EmailConfirmedDate.Value == default) && identityUser.EmailConfirmed))
{
changeType = MemberDataChangeType.FullSave;
anythingChanged = true;
member.EmailConfirmedDate = identityUser.EmailConfirmed ? (DateTime?)DateTime.Now : null;
}
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.Name))
&& member.Name != identityUser.Name && identityUser.Name.IsNullOrWhiteSpace() == false)
{
changeType = MemberDataChangeType.FullSave;
anythingChanged = true;
member.Name = identityUser.Name;
}
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.Email))
&& member.Email != identityUser.Email && identityUser.Email.IsNullOrWhiteSpace() == false)
{
changeType = MemberDataChangeType.FullSave;
anythingChanged = true;
member.Email = identityUser.Email;
}
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.AccessFailedCount))
&& member.FailedPasswordAttempts != identityUser.AccessFailedCount)
{
changeType = MemberDataChangeType.FullSave;
anythingChanged = true;
member.FailedPasswordAttempts = identityUser.AccessFailedCount;
}
if (member.IsLockedOut != identityUser.IsLockedOut)
{
changeType = MemberDataChangeType.FullSave;
anythingChanged = true;
member.IsLockedOut = identityUser.IsLockedOut;
if (member.IsLockedOut)
@@ -665,40 +656,40 @@ namespace Umbraco.Cms.Core.Security
if (member.IsApproved != identityUser.IsApproved)
{
changeType = MemberDataChangeType.FullSave;
anythingChanged = true;
member.IsApproved = identityUser.IsApproved;
}
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.UserName))
&& member.Username != identityUser.UserName && identityUser.UserName.IsNullOrWhiteSpace() == false)
{
changeType = MemberDataChangeType.FullSave;
anythingChanged = true;
member.Username = identityUser.UserName;
}
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.PasswordHash))
&& member.RawPasswordValue != identityUser.PasswordHash && identityUser.PasswordHash.IsNullOrWhiteSpace() == false)
{
changeType = MemberDataChangeType.FullSave;
anythingChanged = true;
member.RawPasswordValue = identityUser.PasswordHash;
member.PasswordConfiguration = identityUser.PasswordConfig;
}
if (member.PasswordConfiguration != identityUser.PasswordConfig)
{
changeType = MemberDataChangeType.FullSave;
anythingChanged = true;
member.PasswordConfiguration = identityUser.PasswordConfig;
}
if (member.SecurityStamp != identityUser.SecurityStamp)
{
changeType = MemberDataChangeType.FullSave;
anythingChanged = true;
member.SecurityStamp = identityUser.SecurityStamp;
}
if (identityUser.IsPropertyDirty(nameof(MemberIdentityUser.Roles)))
{
changeType = MemberDataChangeType.FullSave;
anythingChanged = true;
var identityUserRoles = identityUser.Roles.Select(x => x.RoleId).ToArray();
_memberService.ReplaceRoles(new[] { member.Id }, identityUserRoles);
@@ -707,7 +698,7 @@ namespace Umbraco.Cms.Core.Security
// reset all changes
identityUser.ResetDirtyProperties(false);
return changeType;
return anythingChanged;
}
public IPublishedContent GetPublishedMember(MemberIdentityUser user)
@@ -725,13 +716,6 @@ namespace Umbraco.Cms.Core.Security
return publishedSnapshot.Members.Get(member);
}
private enum MemberDataChangeType
{
None,
LoginOnly,
FullSave
}
/// <summary>
/// Overridden to support Umbraco's own data storage requirements
/// </summary>

View File

@@ -109,13 +109,13 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache
public bool IsLockedOut => Member.IsLockedOut;
public DateTime LastLockoutDate => Member.LastLockoutDate;
public DateTime? LastLockoutDate => Member.LastLockoutDate;
public DateTime CreationDate => Member.CreateDate;
public DateTime LastLoginDate => Member.LastLoginDate;
public DateTime? LastLoginDate => Member.LastLoginDate;
public DateTime LastPasswordChangedDate => Member.LastPasswordChangeDate;
public DateTime? LastPasswordChangedDate => Member.LastPasswordChangeDate;
#endregion
}

View File

@@ -847,7 +847,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
// 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 != default(DateTime))
if (user.LastLoginDate is not null && user.LastLoginDate != default(DateTime))
{
return BadRequest();
}

View File

@@ -64,6 +64,8 @@ namespace Umbraco.Cms.Web.BackOffice.Mapping
//Membership
target.Username = source.Username;
target.Email = source.Email;
target.IsLockedOut = source.IsLockedOut;
target.IsApproved = source.IsApproved;
target.MembershipProperties = _tabsAndPropertiesMapper.MapMembershipProperties(source, context);
}

View File

@@ -290,6 +290,12 @@
case '_umb_membergroup':
saveModel.memberGroups = _.keys(_.pick(prop.value, value => value === true));
break;
case '_umb_approved':
saveModel.isApproved = prop.value;
break;
case '_umb_lockedOut':
saveModel.isLockedOut = prop.value;
break;
}
});

View File

@@ -1916,6 +1916,7 @@ Mange hilsner fra Umbraco robotten
<key alias="stateActive">Aktiv</key>
<key alias="stateDisabled">Deaktiveret</key>
<key alias="stateLockedOut">Låst ude</key>
<key alias="stateApproved">Godkendt</key>
<key alias="stateInvited">Inviteret</key>
<key alias="stateInactive">Inaktiv</key>
<key alias="sortNameAscending">Navn (A-Å)</key>

View File

@@ -1520,7 +1520,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
<key alias="invalidUserPermissionsText">Insufficient user permissions, could not complete the operation</key>
<key alias="operationCancelledHeader">Cancelled</key>
<key alias="operationCancelledText">Operation was cancelled by a 3rd party add-in</key>
<key alias="folderUploadNotAllowed">This file is being uploaded as part of a folder, but creating a new folder is not allowed here</key>
<key alias="folderUploadNotAllowed">This file is being uploaded as part of a folder, but creating a new folder is not allowed here</key>
<key alias="folderCreationNotAllowed">Creating a new folder is not allowed here</key>
<key alias="contentPublishedFailedByEvent">Publishing was cancelled by a 3rd party add-in</key>
<key alias="contentTypeDublicatePropertyType">Property type already exists</key>
@@ -2217,6 +2217,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
<key alias="stateActive">Active</key>
<key alias="stateDisabled">Disabled</key>
<key alias="stateLockedOut">Locked out</key>
<key alias="stateApproved">Approved</key>
<key alias="stateInvited">Invited</key>
<key alias="stateInactive">Inactive</key>
<key alias="sortNameAscending">Name (A-Z)</key>

View File

@@ -2293,6 +2293,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
<key alias="stateActive">Active</key>
<key alias="stateDisabled">Disabled</key>
<key alias="stateLockedOut">Locked out</key>
<key alias="stateApproved">Approved</key>
<key alias="stateInvited">Invited</key>
<key alias="stateInactive">Inactive</key>
<key alias="sortNameAscending">Name (A-Z)</key>

View File

@@ -35,47 +35,11 @@ namespace Umbraco.Cms.Tests.Common.Builders
.WithId(99)
.WithName(Constants.Conventions.Member.StandardPropertiesGroupName)
.AddPropertyType()
.WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextArea)
.WithValueStorageType(ValueStorageType.Ntext)
.WithAlias(Constants.Conventions.Member.Comments)
.WithName(Constants.Conventions.Member.CommentsLabel)
.Done()
.AddPropertyType()
.WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.Boolean)
.WithValueStorageType(ValueStorageType.Integer)
.WithAlias(Constants.Conventions.Member.IsApproved)
.WithName(Constants.Conventions.Member.IsApprovedLabel)
.Done()
.AddPropertyType()
.WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.Boolean)
.WithValueStorageType(ValueStorageType.Integer)
.WithAlias(Constants.Conventions.Member.IsLockedOut)
.WithName(Constants.Conventions.Member.IsLockedOutLabel)
.Done()
.AddPropertyType()
.WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.Label)
.WithValueStorageType(ValueStorageType.Date)
.WithAlias(Constants.Conventions.Member.LastLoginDate)
.WithName(Constants.Conventions.Member.LastLoginDateLabel)
.Done()
.AddPropertyType()
.WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.Label)
.WithValueStorageType(ValueStorageType.Date)
.WithAlias(Constants.Conventions.Member.LastPasswordChangeDate)
.WithName(Constants.Conventions.Member.LastPasswordChangeDateLabel)
.Done()
.AddPropertyType()
.WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.Label)
.WithValueStorageType(ValueStorageType.Date)
.WithAlias(Constants.Conventions.Member.LastLockoutDate)
.WithName(Constants.Conventions.Member.LastLockoutDateLabel)
.Done()
.AddPropertyType()
.WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.Label)
.WithValueStorageType(ValueStorageType.Integer)
.WithAlias(Constants.Conventions.Member.FailedPasswordAttempts)
.WithName(Constants.Conventions.Member.FailedPasswordAttemptsLabel)
.Done();
.WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextArea)
.WithValueStorageType(ValueStorageType.Ntext)
.WithAlias(Constants.Conventions.Member.Comments)
.WithName(Constants.Conventions.Member.CommentsLabel)
.Done();
_propertyGroupBuilders.Add(builder);
return this;
}

View File

@@ -83,12 +83,14 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
IMember member = new Member("xname", "xemail", "xusername", "xrawpassword", memberType, true)
{
LastLoginDate = now,
UpdateDate = now
UpdateDate = now,
};
MemberService.Save(member);
DateTime newDate = now.AddDays(10);
MemberService.SetLastLogin(member.Username, newDate);
member.LastLoginDate = newDate;
member.UpdateDate = newDate;
MemberService.Save(member);
// re-get
member = MemberService.GetById(member.Id);
@@ -97,6 +99,121 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
Assert.That(member.UpdateDate, Is.EqualTo(newDate).Within(1).Seconds);
}
// These might seem like some somewhat pointless tests, but there's some funky things going on in MemberRepository when saving.
[Test]
public void Can_Set_Failed_Password_Attempts()
{
IMemberType memberType = MemberTypeService.Get("member");
IMember member = new Member("xname", "xemail", "xusername", "xrawpassword", memberType, true)
{
FailedPasswordAttempts = 1,
};
MemberService.Save(member);
member.FailedPasswordAttempts = 2;
MemberService.Save(member);
member = MemberService.GetById(member.Id);
Assert.AreEqual(2, member.FailedPasswordAttempts);
}
[Test]
public void Can_Set_Is_Approved()
{
IMemberType memberType = MemberTypeService.Get("member");
IMember member = new Member("xname", "xemail", "xusername", "xrawpassword", memberType, true);
MemberService.Save(member);
member.IsApproved = false;
MemberService.Save(member);
member = MemberService.GetById(member.Id);
Assert.IsFalse(member.IsApproved);
}
[Test]
public void Can_Set_Is_Locked_Out()
{
IMemberType memberType = MemberTypeService.Get("member");
IMember member = new Member("xname", "xemail", "xusername", "xrawpassword", memberType, true)
{
IsLockedOut = false,
};
MemberService.Save(member);
member.IsLockedOut = true;
MemberService.Save(member);
member = MemberService.GetById(member.Id);
Assert.IsTrue(member.IsLockedOut);
}
[Test]
public void Can_Set_Last_Lockout_Date()
{
DateTime now = DateTime.Now;
IMemberType memberType = MemberTypeService.Get("member");
IMember member = new Member("xname", "xemail", "xusername", "xrawpassword", memberType, true)
{
LastLockoutDate = now,
};
MemberService.Save(member);
DateTime newDate = now.AddDays(10);
member.LastLockoutDate = newDate;
MemberService.Save(member);
// re-get
member = MemberService.GetById(member.Id);
Assert.That(member.LastLockoutDate, Is.EqualTo(newDate).Within(1).Seconds);
}
[Test]
public void Can_set_Last_Login_Date()
{
DateTime now = DateTime.Now;
IMemberType memberType = MemberTypeService.Get("member");
IMember member = new Member("xname", "xemail", "xusername", "xrawpassword", memberType, true)
{
LastLoginDate = now,
};
MemberService.Save(member);
DateTime newDate = now.AddDays(10);
member.LastLoginDate = newDate;
MemberService.Save(member);
// re-get
member = MemberService.GetById(member.Id);
Assert.That(member.LastLoginDate, Is.EqualTo(newDate).Within(1).Seconds);
}
[Test]
public void Can_Set_Last_Password_Change_Date()
{
DateTime now = DateTime.Now;
IMemberType memberType = MemberTypeService.Get("member");
IMember member = new Member("xname", "xemail", "xusername", "xrawpassword", memberType, true)
{
LastPasswordChangeDate = now,
};
MemberService.Save(member);
DateTime newDate = now.AddDays(10);
member.LastPasswordChangeDate = newDate;
MemberService.Save(member);
// re-get
member = MemberService.GetById(member.Id);
Assert.That(member.LastPasswordChangeDate, Is.EqualTo(newDate).Within(1).Seconds);
}
[Test]
public void Can_Create_Member_With_Properties()
{
@@ -116,17 +233,9 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
// contains the umbracoMember... properties created when installing, on the member type
// contains the other properties, that PublishedContentType adds (BuiltinMemberProperties)
//
// TODO: see TODO in PublishedContentType, this list contains duplicates
string[] aliases = new[]
{
Constants.Conventions.Member.Comments,
Constants.Conventions.Member.FailedPasswordAttempts,
Constants.Conventions.Member.IsApproved,
Constants.Conventions.Member.IsLockedOut,
Constants.Conventions.Member.LastLockoutDate,
Constants.Conventions.Member.LastLoginDate,
Constants.Conventions.Member.LastPasswordChangeDate,
nameof(IMember.Email),
nameof(IMember.Username),
nameof(IMember.Comments),
@@ -589,14 +698,25 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
// just a c# property of the Member object
resolved.Email = "changed@test.com";
// NOTE: This will not trigger a property isDirty for the same reason above, but this is a new change, so leave this to make sure.
resolved.FailedPasswordAttempts = 1234;
// NOTE: this WILL trigger a property isDirty because setting this c# property actually sets a value of
// the underlying 'Property'
resolved.FailedPasswordAttempts = 1234;
resolved.Comments = "This will make it dirty";
var dirtyMember = (ICanBeDirty)resolved;
var dirtyProperties = resolved.Properties.Where(x => x.IsDirty()).ToList();
Assert.IsTrue(dirtyMember.IsDirty());
Assert.AreEqual(1, dirtyProperties.Count);
// Assert that email and failed password attempts is still set as dirty on the member it self
Assert.IsTrue(dirtyMember.IsPropertyDirty(nameof(resolved.Email)));
Assert.IsTrue(dirtyMember.IsPropertyDirty(nameof(resolved.FailedPasswordAttempts)));
// Comment will also be marked as dirty on the member object because content base merges dirty properties.
Assert.IsTrue(dirtyMember.IsPropertyDirty(Constants.Conventions.Member.Comments));
Assert.AreEqual(3, dirtyMember.GetDirtyProperties().Count());
}
[Test]
@@ -1205,7 +1325,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
MemberService.Save(members);
Member customMember = MemberBuilder.CreateSimpleMember(memberType, "hello", "hello@test.com", "hello", "hello");
customMember.SetValue(Constants.Conventions.Member.IsLockedOut, true);
customMember.IsLockedOut = true;
MemberService.Save(customMember);
int found = MemberService.GetCount(MemberCountType.LockedOut);
@@ -1222,7 +1342,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
MemberService.Save(members);
Member customMember = MemberBuilder.CreateSimpleMember(memberType, "hello", "hello@test.com", "hello", "hello");
customMember.SetValue(Constants.Conventions.Member.IsApproved, false);
customMember.IsApproved = false;
MemberService.Save(customMember);
int found = MemberService.GetCount(MemberCountType.Approved);
@@ -1250,48 +1370,6 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
Assert.IsTrue(found.Comments.IsNullOrWhiteSpace());
}
/// <summary>
/// Because we are forcing some of the built-ins to be Labels which have an underlying db type as nvarchar but we need
/// to ensure that the dates/int get saved to the correct column anyways.
/// </summary>
[Test]
public void Setting_DateTime_Property_On_Built_In_Member_Property_Saves_To_Correct_Column()
{
IMemberType memberType = MemberTypeBuilder.CreateSimpleMemberType();
MemberTypeService.Save(memberType);
Member member = MemberBuilder.CreateSimpleMember(memberType, "test", "test@test.com", "test", "test");
DateTime date = DateTime.Now;
member.LastLoginDate = DateTime.Now;
MemberService.Save(member);
IMember result = MemberService.GetById(member.Id);
Assert.AreEqual(
date.TruncateTo(DateTimeExtensions.DateTruncate.Second),
result.LastLoginDate.TruncateTo(DateTimeExtensions.DateTruncate.Second));
// now ensure the col is correct
ISqlContext sqlContext = GetRequiredService<ISqlContext>();
Sql<ISqlContext> sql = sqlContext.Sql().Select<PropertyDataDto>()
.From<PropertyDataDto>()
.InnerJoin<PropertyTypeDto>().On<PropertyDataDto, PropertyTypeDto>(dto => dto.PropertyTypeId, dto => dto.Id)
.InnerJoin<ContentVersionDto>().On<PropertyDataDto, ContentVersionDto>((left, right) => left.VersionId == right.Id)
.Where<ContentVersionDto>(dto => dto.NodeId == member.Id)
.Where<PropertyTypeDto>(dto => dto.Alias == Constants.Conventions.Member.LastLoginDate);
List<PropertyDataDto> colResult;
using (IScope scope = ScopeProvider.CreateScope())
{
colResult = ScopeAccessor.AmbientScope.Database.Fetch<PropertyDataDto>(sql);
scope.Complete();
}
Assert.AreEqual(1, colResult.Count);
Assert.IsTrue(colResult.First().DateValue.HasValue);
Assert.IsFalse(colResult.First().IntegerValue.HasValue);
Assert.IsNull(colResult.First().TextValue);
Assert.IsNull(colResult.First().VarcharValue);
}
[Test]
public void New_Member_Approved_By_Default()
{

View File

@@ -137,14 +137,14 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Tests.Common.Builders
Assert.AreEqual(testLastLoginDate, member.LastLoginDate);
Assert.AreEqual(testLastPasswordChangeDate, member.LastPasswordChangeDate);
Assert.AreEqual(testGroups, member.Groups.ToArray());
Assert.AreEqual(10, member.Properties.Count); // 7 from membership properties group, 3 custom
Assert.AreEqual(4, member.Properties.Count); // 1 from membership properties group, 3 custom
Assert.AreEqual(testPropertyData1.Value, member.GetValue<string>(testPropertyData1.Key));
Assert.AreEqual(testPropertyData2.Value, member.GetValue<string>(testPropertyData2.Key));
Assert.AreEqual(testPropertyData3.Value, member.GetValue<string>(testPropertyData3.Key));
IOrderedEnumerable<int> propertyIds = member.Properties.Select(x => x.Id).OrderBy(x => x);
Assert.AreEqual(testPropertyIdsIncrementingFrom + 1, propertyIds.Min());
Assert.AreEqual(testPropertyIdsIncrementingFrom + 10, propertyIds.Max());
Assert.AreEqual(testPropertyIdsIncrementingFrom + 4, propertyIds.Max());
Assert.AreEqual(2, member.AdditionalData.Count);
Assert.AreEqual(testAdditionalData1.Value, member.AdditionalData[testAdditionalData1.Key]);

View File

@@ -100,11 +100,11 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Tests.Common.Builders
Assert.AreEqual(testThumbnail, memberType.Thumbnail);
Assert.AreEqual(testTrashed, memberType.Trashed);
Assert.IsFalse(memberType.IsContainer);
Assert.AreEqual(9, memberType.PropertyTypes.Count()); // 7 from membership properties group, 2 custom
Assert.AreEqual(3, memberType.PropertyTypes.Count()); // 1 from membership properties group, 2 custom
IOrderedEnumerable<int> propertyTypeIds = memberType.PropertyTypes.Select(x => x.Id).OrderBy(x => x);
Assert.AreEqual(testPropertyTypeIdsIncrementingFrom + 1, propertyTypeIds.Min());
Assert.AreEqual(testPropertyTypeIdsIncrementingFrom + 9, propertyTypeIds.Max());
Assert.AreEqual(testPropertyTypeIdsIncrementingFrom + 3, propertyTypeIds.Max());
Assert.IsTrue(memberType.MemberCanEditProperty(testPropertyType1.Alias));
Assert.IsFalse(memberType.MemberCanViewProperty(testPropertyType1.Alias));

View File

@@ -594,7 +594,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers
Path = member.Path
};
memberDisplay = new MemberDisplay()
memberDisplay = new MemberDisplay
{
Id = memberId,
SortOrder = member.SortOrder,
@@ -609,33 +609,57 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers
ContentTypeName = member.ContentType.Name,
Icon = fakeMemberData.Icon,
Path = member.Path,
Tabs = new List<Tab<ContentPropertyDisplay>>()
Tabs = new List<Tab<ContentPropertyDisplay>>
{
new Tab<ContentPropertyDisplay>()
new()
{
Alias = "test",
Id = 77,
Properties = new List<ContentPropertyDisplay>()
Properties = new List<ContentPropertyDisplay>
{
new ContentPropertyDisplay()
new()
{
Alias = "_umb_login"
Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login",
},
new ContentPropertyDisplay()
new()
{
Alias= "_umb_email"
Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email",
},
new ContentPropertyDisplay()
new()
{
Alias = "_umb_password"
Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password",
},
new ContentPropertyDisplay()
new()
{
Alias = "_umb_membergroup"
}
}
}
}
Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}membergroup",
},
new()
{
Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}failedPasswordAttempts",
},
new()
{
Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}approved",
},
new()
{
Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}lockedOut",
},
new()
{
Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}lastLockoutDate",
},
new()
{
Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}lastLoginDate",
},
new()
{
Alias = $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}lastPasswordChangeDate",
},
},
},
},
};
return member;