Skip to content

Commit

Permalink
Merge branch 'release/6.26.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
vc-ci committed Aug 28, 2023
2 parents e9a4f99 + 3f40666 commit 0d0253c
Show file tree
Hide file tree
Showing 9 changed files with 133 additions and 59 deletions.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<Authors>VirtoCommerce</Authors>
</PropertyGroup>
<PropertyGroup>
<VersionPrefix>6.25.0</VersionPrefix>
<VersionPrefix>6.26.0</VersionPrefix>
<VersionSuffix></VersionSuffix>
<VersionSuffix Condition=" '$(VersionSuffix)' != '' AND '$(BuildNumber)' != '' ">$(VersionSuffix)-$(BuildNumber)</VersionSuffix>
</PropertyGroup>
Expand Down
55 changes: 40 additions & 15 deletions VirtoCommerce.Storefront.Model/Security/SecurityErrorDescriber.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ public static FormError UsernameOrEmailIsRequired()
return new FormError
{
Code = nameof(UsernameOrEmailIsRequired).PascalToKebabCase(),
Description = "Username or email is required"
Description = "Please provide a username or email"
};
}
public static FormError LoginFailed()
{
return new FormError
{
Code = nameof(LoginFailed).PascalToKebabCase(),
Description = "Login attempt failed"
Description = "Login attempt failed. Please check your credentials."
};
}

Expand All @@ -26,15 +26,15 @@ public static FormError UserNotFound()
return new FormError
{
Code = nameof(UserNotFound).PascalToKebabCase(),
Description = "User not found"
Description = "User not found. Please ensure you've entered the correct information."
};
}
public static FormError UserCannotLoginInStore()
{
return new FormError
{
Code = nameof(UserCannotLoginInStore).PascalToKebabCase(),
Description = "User cannot login to current store"
Description = "Access denied. You cannot sign in to the current store"
};
}

Expand All @@ -43,7 +43,7 @@ public static FormError PhoneNumberNotFound()
return new FormError
{
Code = nameof(PhoneNumberNotFound).PascalToKebabCase(),
Description = "Reset password by code is Failed. Phone Number not found"
Description = "Password reset failed. Phone number not found for verification."
};
}

Expand All @@ -52,7 +52,16 @@ public static FormError AccountIsBlocked()
return new FormError
{
Code = nameof(AccountIsBlocked).PascalToKebabCase(),
Description = "Account is blocked"
Description = "Your account has been blocked. Please contact support for assistance."
};
}

public static FormError EmailVerificationIsRequired()
{
return new FormError
{
Code = nameof(EmailVerificationIsRequired).PascalToKebabCase(),
Description = "Email verification required. Please verify your email address."
};
}

Expand All @@ -61,7 +70,7 @@ public static FormError OperationFailed()
return new FormError
{
Code = nameof(OperationFailed).PascalToKebabCase(),
Description = "Operation failed"
Description = "Oops, something went wrong. The operation could not be completed."
};
}

Expand All @@ -70,7 +79,7 @@ public static FormError ResetPasswordIsTurnedOff()
return new FormError
{
Code = nameof(ResetPasswordIsTurnedOff).PascalToKebabCase(),
Description = "Reset password by code is turned off"
Description = "Password reset by code is currently unavailable."
};
}

Expand All @@ -79,47 +88,47 @@ public static FormError InvalidToken()
return new FormError
{
Code = nameof(InvalidToken).PascalToKebabCase(),
Description = "Token is invalid or expired"
Description = "Sorry, the token is invalid or has expired. Please request a new one."
};
}
public static FormError InvalidUrl()
{
return new FormError
{
Code = nameof(InvalidUrl).PascalToKebabCase(),
Description = "Url is invalid"
Description = "The URL you provided is not valid."
};
}
public static FormError ResetPasswordInvalidData()
{
return new FormError
{
Code = nameof(ResetPasswordInvalidData).PascalToKebabCase(),
Description = "Reset password data is invalid"
Description = "Password reset data is invalid. Please try again."
};
}
public static FormError PasswordAndConfirmPasswordDoesNotMatch()
{
return new FormError
{
Code = nameof(PasswordAndConfirmPasswordDoesNotMatch).PascalToKebabCase(),
Description = "Password and Confirm password doesn't match"
Description = "Passwords don't match. Please ensure both passwords are the same."
};
}
public static FormError InvitationHasAreadyBeenUsed()
{
return new FormError
{
Code = nameof(InvitationHasAreadyBeenUsed).PascalToKebabCase(),
Description = "Invitation has already been used"
Description = "This invitation has already been used. Please contact the sender if you need assistance."
};
}
public static FormError PhoneNumberVerificationFailed()
{
return new FormError
{
Code = nameof(PhoneNumberVerificationFailed).PascalToKebabCase(),
Description = "Phone number verification failed"
Description = "Phone number verification failed. Please try again or contact support."
};
}
public static FormError ErrorSendNotification(string error)
Expand All @@ -135,7 +144,23 @@ public static FormError UserIsLockedOut()
return new FormError
{
Code = nameof(UserIsLockedOut).PascalToKebabCase(),
Description = "User is locked out"
Description = "Your account has been locked. Please contact support for assistance."
};
}
public static FormError UserIsTemporaryLockedOut()
{
return new FormError
{
Code = nameof(UserIsLockedOut).PascalToKebabCase(),
Description = "Your account has been temporarily locked. Please try again after some time."
};
}
public static FormError PasswordExpired()
{
return new FormError
{
Code = nameof(PasswordExpired).PascalToKebabCase(),
Description = "Your password has been expired and must be changed."
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using VirtoCommerce.Storefront.Model.Common.Specifications;

namespace VirtoCommerce.Storefront.Model.Security.Specifications
{
public class IsUserLockedByRequiredEmailVerificationSpecification : ISpecification<User>
{
public virtual bool IsSatisfiedBy(User user)
{
return user.Contact.Status == "Locked" && !user.EmailConfirmed;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;
using VirtoCommerce.Storefront.Model.Common.Specifications;

namespace VirtoCommerce.Storefront.Model.Security.Specifications
{
public class IsUserTemporaryLockedOutSpecification : ISpecification<User>
{
public virtual bool IsSatisfiedBy(User user)
{
return user.LockoutEndDateUtc != DateTime.MaxValue.ToUniversalTime();
}
}
}
7 changes: 7 additions & 0 deletions VirtoCommerce.Storefront.Model/Security/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,14 @@ public bool IsLockedOut
/// The flag indicates that the user is an administrator
/// </summary>
public bool IsAdministrator { get; set; }

public string UserType { get; set; }

/// <summary>
/// Represents user status.
/// </summary>
public string Status { get; set; }

public AccountState UserState { get; set; }
/// <summary>
/// The user ID of an operator who has loggen in on behalf of a customer
Expand Down
75 changes: 45 additions & 30 deletions VirtoCommerce.Storefront/Controllers/Api/ApiAccountController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,59 +88,74 @@ public async Task<ActionResult<UserActionIdentityResult>> Login([FromBody] Login
return UserActionIdentityResult.Failed(SecurityErrorDescriber.UsernameOrEmailIsRequired());
}

var result = CheckLoginUser(user);

if (result != UserActionIdentityResult.Success)
if (user == null)
{
return result;
return UserActionIdentityResult.Failed(SecurityErrorDescriber.LoginFailed());
}

var loginResult = await _signInManager.PasswordSignInAsync(user.UserName, login.Password, login.RememberMe, lockoutOnFailure: true);

if (!loginResult.Succeeded)
{
result = UserActionIdentityResult.Failed(SecurityErrorDescriber.LoginFailed());
if (loginResult.IsLockedOut)
{
if (new IsUserLockedByRequiredEmailVerificationSpecification().IsSatisfiedBy(user))
{
return UserActionIdentityResult.Failed(SecurityErrorDescriber.EmailVerificationIsRequired());
}
if (new IsUserTemporaryLockedOutSpecification().IsSatisfiedBy(user))
{
return UserActionIdentityResult.Failed(SecurityErrorDescriber.UserIsTemporaryLockedOut());
}

return UserActionIdentityResult.Failed(SecurityErrorDescriber.UserIsLockedOut());
}

return UserActionIdentityResult.Failed(SecurityErrorDescriber.LoginFailed());
}
else
{
await SetLastLoginDate(user);
await _publisher.Publish(new UserLoginEvent(WorkContext, user));
if (string.IsNullOrEmpty(returnUrl))
if (user.Contact == null)
{
return result;
ResetIdentityCookies();

return UserActionIdentityResult.Failed(SecurityErrorDescriber.UserNotFound());
}

var newUrl = Url.IsLocalUrl(returnUrl) ? returnUrl : "~/";
result.ReturnUrl = UrlBuilder.ToAppRelative(newUrl, WorkContext.CurrentStore, WorkContext.CurrentLanguage);
}
return result;
}
if (new IsUserPasswordExpiredSpecification().IsSatisfiedBy(user))
{
ResetIdentityCookies();

private UserActionIdentityResult CheckLoginUser(User user)
{
if (user == null)
{
return UserActionIdentityResult.Failed(SecurityErrorDescriber.LoginFailed());
}
return UserActionIdentityResult.Failed(SecurityErrorDescriber.PasswordExpired());
}

if (!new CanUserLoginToStoreSpecification(user).IsSatisfiedBy(WorkContext.CurrentStore))
{
return UserActionIdentityResult.Failed(SecurityErrorDescriber.UserCannotLoginInStore());
}
if (!new CanUserLoginToStoreSpecification(user).IsSatisfiedBy(WorkContext.CurrentStore))
{
ResetIdentityCookies();

return UserActionIdentityResult.Failed(SecurityErrorDescriber.UserCannotLoginInStore());
}

if (new IsUserLockedOutSpecification().IsSatisfiedBy(user))
{
return UserActionIdentityResult.Failed(SecurityErrorDescriber.UserIsLockedOut());
}

if (new IsUserSuspendedSpecification().IsSatisfiedBy(user))
await SetLastLoginDate(user);
await _publisher.Publish(new UserLoginEvent(WorkContext, user));

var result = UserActionIdentityResult.Success;

if (!string.IsNullOrEmpty(returnUrl))
{
return UserActionIdentityResult.Failed(SecurityErrorDescriber.AccountIsBlocked());
var newUrl = Url.IsLocalUrl(returnUrl) ? returnUrl : "~/";
result.ReturnUrl = UrlBuilder.ToAppRelative(newUrl, WorkContext.CurrentStore, WorkContext.CurrentLanguage);
}

return UserActionIdentityResult.Success;
return result;
}

private void ResetIdentityCookies()
{
Response.Cookies.Delete(".AspNetCore.Identity.Application");
}

// POST: storefrontapi/account/user
[HttpPost("user")]
Expand Down
6 changes: 4 additions & 2 deletions VirtoCommerce.Storefront/Domain/Security/SecurityConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public static dto.Role ToRoleDto(this Role role)
{
Id = role.Id,
Name = role.Name,
Permissions = role?.Permissions.Select(x => new dto.Permission { Id = x, Name = x }).ToList()
Permissions = role.Permissions.Select(x => new dto.Permission { Id = x, Name = x }).ToList()
};
}
public static Role ToRole(this dto.Role roleDto)
Expand All @@ -33,7 +33,7 @@ public static Role ToRole(this dto.Role roleDto)
{
Id = roleDto.Id,
Name = roleDto.Name,
Permissions = roleDto?.Permissions.Select(x => x.Id).ToList()
Permissions = roleDto.Permissions.Select(x => x.Id).ToList()
};
}

Expand Down Expand Up @@ -97,6 +97,7 @@ public static User ToUser(this dto.ApplicationUser userDto)
UserType = userDto.UserType,
TwoFactorEnabled = userDto.TwoFactorEnabled ?? false,
PhoneNumberConfirmed = userDto.PhoneNumberConfirmed ?? false,
Status = userDto.Status,
};

if (!userDto.Roles.IsNullOrEmpty())
Expand Down Expand Up @@ -144,6 +145,7 @@ public static dto.ApplicationUser ToUserDto(this User user)
PhoneNumber = user.PhoneNumber,
PhoneNumberConfirmed = user.PhoneNumberConfirmed,
PasswordExpired = user.PasswordExpired,
Status = user.Status
};

if (!user.Roles.IsNullOrEmpty())
Expand Down
18 changes: 9 additions & 9 deletions VirtoCommerce.Storefront/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:2082/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
Expand All @@ -22,7 +14,15 @@
"ASPNETCORE_preventHostingStartup": "True",
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:2083/"
"applicationUrl": "https://localhost:4302/"
}
},
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:2082/",
"sslPort": 0
}
}
}
4 changes: 2 additions & 2 deletions VirtoCommerce.Storefront/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
//"RedisConnectionString": "127.0.0.1:6379,ssl=False"
},
"VirtoCommerce": {
"DefaultStore": "Electronics",
"DefaultStore": "B2B-store",
"StoreUrls": {
// Define mapping of concrete stores with public urls in this section
//"{store id}":"{store public url}"
Expand All @@ -22,7 +22,7 @@
},
"PageSizeMaxValue": 100,
"Endpoint": {
"Url": "http://localhost:10645/",
"Url": "https://localhost:5001/",
// Use AppId and SecretKey for Platform API authentication (has higher priority than UserName/Password)
//"AppId": "27e0d789f12641049bd0e939185b4fd2",
//"SecretKey": "34f0a3c12c9dbb59b63b5fece955b7b2b9a3b20f84370cba1524dd5c53503a2e2cb733536ecf7ea1e77319a47084a3a2c9d94d36069a432ecc73b72aeba6ea78",
Expand Down

0 comments on commit 0d0253c

Please sign in to comment.