722 lines
25 KiB
C#
722 lines
25 KiB
C#
using System.Collections.Generic;
|
|
using System.Threading.Tasks;
|
|
using xCommons.Extensions;
|
|
using xExceptions.Constants;
|
|
using xIdentityModels.Constants;
|
|
using xIdentityModels.Extensions;
|
|
using xIdentityModels.Models;
|
|
using xIdentityModels.Navigations;
|
|
|
|
namespace xIds.Providers
|
|
{
|
|
public partial class XIdentityManager
|
|
{
|
|
//
|
|
#region Action Handlers ...
|
|
/// <summary>
|
|
/// Request an Action
|
|
/// </summary>
|
|
/// <param name="lang">specify destination language</param>
|
|
/// <param name="device">an instance of <see>XDevice</see></param>
|
|
/// <param name="userSelectByParam">specified user's identifier</param>
|
|
/// <param name="step">a member of <see>XAction</see> which represent Action Step</param>
|
|
/// <param name="context">an instance of <see>XActionRequestContext</see> which provides required informations for specified step</param>
|
|
/// <param name="forceCheckContext">specify checking context, default is false</param>
|
|
/// <param name="forceCheckUserExists">specify check user exists, default is true</param>
|
|
/// <param name="forceCheckUserDevice">specify check user and device relation, default is false</param>
|
|
/// <param name="forceRenewToken">specifies force renew Action Token if it's expired, default is false</param>
|
|
/// <returns>an instance of <see>XActionResponse</see></returns>
|
|
private async Task<XActionResponse> ActionRequest(
|
|
string lang,
|
|
XDevice device,
|
|
string userSelectByParam,
|
|
XAction step,
|
|
XActionRequestContext context = null,
|
|
bool forceCheckContext = false,
|
|
bool forceCheckUserExists = true,
|
|
bool forceCheckUserDevice = false,
|
|
bool forceRenewToken = false
|
|
)
|
|
{
|
|
//
|
|
// Validate Args ...
|
|
ValidationProvider.NotEmpty(lang);
|
|
ValidationProvider.NotNull(device);
|
|
|
|
//
|
|
// Check Device not Banned ...
|
|
await ValidateDeviceForActions(device);
|
|
|
|
//
|
|
// Check User Exists and Device Relation ...
|
|
if (forceCheckUserExists)
|
|
{
|
|
if (forceCheckUserDevice)
|
|
{
|
|
//
|
|
if (context.Device != null &&
|
|
!device.IsSameAs(context.Device))
|
|
{
|
|
XException.InvalidDevice.Throw();
|
|
}
|
|
|
|
//
|
|
await ValidateUserAndDeviceRelation(userSelectByParam, device);
|
|
}
|
|
else
|
|
{
|
|
await ValidateUserExistsAsync(userSelectByParam);
|
|
}
|
|
}
|
|
|
|
//
|
|
XToken xToken = null;
|
|
XActionResponse result = null;
|
|
XActionRequestToken xRequest = null;
|
|
|
|
//
|
|
var isExists = false;
|
|
var forceUserSelectByParamNotEmpty = true;
|
|
switch (step)
|
|
{
|
|
case XAction.Registration:
|
|
//
|
|
forceUserSelectByParamNotEmpty = false;
|
|
isExists = await IsTokenExistsByDeviceAndType(device, step);
|
|
|
|
//
|
|
if (isExists)
|
|
{
|
|
xToken = await ValidateAndRetieveTokenByDeviceAndType(device, step);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
//
|
|
isExists = await IsTokenExistsByUserAndType(userSelectByParam, step);
|
|
|
|
//
|
|
if (isExists)
|
|
{
|
|
xToken = await ValidateAndRetieveTokenByUserAndType(userSelectByParam, step);
|
|
}
|
|
break;
|
|
}
|
|
|
|
//
|
|
if (isExists)
|
|
{
|
|
//
|
|
var isValidToken = IsValidToken(xToken.Token);
|
|
if (!isValidToken && !forceRenewToken)
|
|
{
|
|
switch (step)
|
|
{
|
|
case XAction.Registration:
|
|
case XAction.Invite:
|
|
forceRenewToken = true;
|
|
break;
|
|
|
|
default:
|
|
throw XException.InvalidToken.ToException();
|
|
}
|
|
}
|
|
|
|
//
|
|
if (!isValidToken && forceRenewToken)
|
|
{
|
|
await ValidateAndHandleToken(xToken.Token);
|
|
return await ActionRequest(
|
|
lang,
|
|
device,
|
|
userSelectByParam,
|
|
step,
|
|
context,
|
|
forceCheckContext,
|
|
forceCheckUserExists,
|
|
forceCheckUserDevice,
|
|
forceRenewToken);
|
|
}
|
|
|
|
//
|
|
xRequest = ValidateAndParseActionToken(xToken.Token);
|
|
|
|
//
|
|
// Check Exists Context ...
|
|
if (forceCheckContext)
|
|
{
|
|
//
|
|
var isSameContext = xRequest.Context.IsSameAs(context);
|
|
if (!isSameContext)
|
|
{
|
|
XException.InvalidData.Throw();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//
|
|
if (context != null)
|
|
{
|
|
//
|
|
var contextUserSelectByParam = GetUserSelectByParam(
|
|
context,
|
|
false,
|
|
new List<XUserSelectBy> {
|
|
XUserSelectBy.ID,
|
|
XUserSelectBy.Email,
|
|
XUserSelectBy.MobileNumber
|
|
});
|
|
var contextLang = context.Lang;
|
|
var contextDevice = context.Device;
|
|
|
|
//
|
|
// prevent from issues on Registration Request ...
|
|
if (forceUserSelectByParamNotEmpty)
|
|
{
|
|
var userSelectByType = GetUserSelectByType(userSelectByParam);
|
|
var contextUserSelectByType = GetUserSelectByType(contextUserSelectByParam);
|
|
if (contextUserSelectByType != userSelectByType)
|
|
{
|
|
//
|
|
var excludesList = GenerateUserSelectByExcludes(new List<XUserSelectBy> { userSelectByType });
|
|
|
|
//
|
|
contextUserSelectByParam = GetUserSelectByParam(context, false, excludesList);
|
|
}
|
|
}
|
|
|
|
//
|
|
// Fix Device Not Same on Invitation and Request
|
|
if (step == XAction.Registration)
|
|
{
|
|
//
|
|
context.Device = device;
|
|
contextDevice = context.Device;
|
|
}
|
|
|
|
//
|
|
// Validate Context Data must be Same as Request Data ...
|
|
if ((!contextUserSelectByParam.IsNullOrEmpty() &&
|
|
contextUserSelectByParam != userSelectByParam) ||
|
|
(!contextLang.IsNullOrEmpty() &&
|
|
contextLang != lang) ||
|
|
(contextDevice != null &&
|
|
!contextDevice.IsSameAs(device)))
|
|
{
|
|
XException.InvalidData.Throw();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//
|
|
context = new XActionRequestContext
|
|
{
|
|
Lang = lang,
|
|
Device = device
|
|
};
|
|
|
|
//
|
|
// Add Content ...
|
|
var userSelectByType = GetUserSelectByType(
|
|
userSelectByParam,
|
|
forceUserSelectByParamNotEmpty);
|
|
switch (userSelectByType)
|
|
{
|
|
case XUserSelectBy.MobileNumber:
|
|
context.MobileNumber = userSelectByParam;
|
|
break;
|
|
|
|
case XUserSelectBy.Email:
|
|
context.Email = userSelectByParam;
|
|
break;
|
|
|
|
case XUserSelectBy.Username:
|
|
context.UserName = userSelectByParam;
|
|
break;
|
|
|
|
case XUserSelectBy.ID:
|
|
context.UserId = userSelectByParam;
|
|
break;
|
|
|
|
case XUserSelectBy.NotSpecified:
|
|
default:
|
|
if (forceCheckUserExists)
|
|
{
|
|
XException.InvalidArgs.Throw();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Generate Registration Request ...
|
|
xRequest = new XActionRequestToken
|
|
{
|
|
Action = step,
|
|
Context = context
|
|
};
|
|
|
|
//
|
|
// Add User Id if Exists to Context ...
|
|
var isUserExists = await IsUserExistsAsync(userSelectByParam);
|
|
if (isUserExists)
|
|
{
|
|
//
|
|
var user = await GetUserAsync(userSelectByParam);
|
|
if (user != null)
|
|
{
|
|
xRequest.Context.UserId = user.Id;
|
|
}
|
|
}
|
|
|
|
//
|
|
xRequest.Prepare(Configuration.IdentitySecretKey);
|
|
|
|
//
|
|
xToken = await HashRequest(
|
|
xRequest,
|
|
forceUserSelectByParamNotEmpty);
|
|
}
|
|
|
|
//
|
|
// When XToken Exists Passed XToken ...
|
|
result = GetActionResultResponse(xToken);
|
|
|
|
//
|
|
if (result == null)
|
|
{
|
|
XException.ActionFailed.Throw();
|
|
}
|
|
|
|
//
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request a Mobile Verification Code
|
|
/// </summary>
|
|
/// <param name="lang">specify destination language</param>
|
|
/// <param name="device">an instance of <see>XDevice</see></param>
|
|
/// <param name="actionToken">a token which approved user action</param>
|
|
/// <param name="mobileNumber">the mobile number which is going to request a verification code</param>
|
|
/// <param name="checkMobileInUse">check mobile number is in use or not, default is true</param>
|
|
/// <returns>an instance of <see>XActionResponse</see></returns>
|
|
private async Task<XActionResponse> RequestMobileVerificationCode(
|
|
string lang,
|
|
XDevice device,
|
|
string actionToken,
|
|
string mobileNumber,
|
|
bool checkMobileInUse = true
|
|
)
|
|
{
|
|
//
|
|
// Validate Args ...
|
|
await ValidationProvider
|
|
.GroupValidationBuilder()
|
|
.AddNotEmpty(lang, actionToken, mobileNumber)
|
|
.AddMobileNumber(mobileNumber)
|
|
.AddNotNull(device)
|
|
.ValidateGroupAsync();
|
|
|
|
//
|
|
// Check Device Validation ...
|
|
await ValidateDeviceForActions(device);
|
|
|
|
//
|
|
// Check Mobile is Unique ...
|
|
if (checkMobileInUse)
|
|
{
|
|
var isUserExists = await IsUserExistsAsync(mobileNumber);
|
|
if (isUserExists)
|
|
{
|
|
//
|
|
var xUser = await GetUserAsync(mobileNumber);
|
|
|
|
//
|
|
if (xUser.PhoneNumberConfirmed)
|
|
{
|
|
XException.MobileInUsed.Throw();
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// Get Related Token to Registration Hash and Validate it,
|
|
// then Parse XActionRequest Instance from Token ...
|
|
var request = await ValidateAndParseActionHash(actionToken);
|
|
|
|
//
|
|
// Validate two Device Must Same ...
|
|
ValidateRequestAndGiveDevices(device, request.Context.Device);
|
|
|
|
//
|
|
// Extract UserSelectByParam from XActionRequest ...
|
|
var userSelectByParam = GetUserSelectByParam(
|
|
request,
|
|
forceNotNull: false,
|
|
excludes: new List<XUserSelectBy> {
|
|
XUserSelectBy.ID,
|
|
XUserSelectBy.Email,
|
|
XUserSelectBy.MobileNumber
|
|
});
|
|
|
|
//
|
|
// if it's null, means user doesn't have Invitation Token
|
|
// and there is no User Selector ...
|
|
if (userSelectByParam.IsNullOrEmpty())
|
|
{
|
|
//
|
|
// Since Mobile is Unique so it can act as a
|
|
// UserSelector ...
|
|
userSelectByParam = mobileNumber;
|
|
}
|
|
|
|
//
|
|
// Define empty Verification Code
|
|
// and Empty Verification Request ...
|
|
var verificationCode = "";
|
|
XVerificationRequest xVerificationRequest = null;
|
|
|
|
//
|
|
// Check Device Request Verification Code before or Not ...
|
|
var isDeviceRequested = await IsDeviceRequestedForVerificationCodeBefor(device);
|
|
if (isDeviceRequested)
|
|
{
|
|
//
|
|
// if Requested before, Retrieve XVerification instance by Device from Db
|
|
// and Validate it ...
|
|
xVerificationRequest = await ValidateAndRetrieveVerificationRequest(device);
|
|
|
|
//
|
|
if (xVerificationRequest.VerificationCode.IsNullOrEmpty())
|
|
{
|
|
//
|
|
verificationCode = GenerateRandom().ToString();
|
|
xVerificationRequest.VerificationCode = verificationCode;
|
|
|
|
//
|
|
await RemoveVerificationCodeRequestByDevice(
|
|
device,
|
|
saveChanges: false
|
|
);
|
|
await AddVerificationRequest(
|
|
xVerificationRequest,
|
|
saveChanges: false
|
|
);
|
|
|
|
//
|
|
await DbContext.SaveChangesAsync();
|
|
}
|
|
|
|
//
|
|
// if XVerificationRequest is Valid, it must Contain an Alive Verification Code
|
|
// so retrieve it and assign it ...
|
|
verificationCode = xVerificationRequest.VerificationCode;
|
|
}
|
|
else
|
|
{
|
|
//
|
|
verificationCode = GenerateRandom().ToString();
|
|
}
|
|
|
|
//
|
|
// if it's First Verification Request of given Device
|
|
// Generate a new Verification Code ...
|
|
if (verificationCode.IsNullOrEmpty())
|
|
{
|
|
verificationCode = GenerateRandom().ToString();
|
|
}
|
|
|
|
//
|
|
// Check Context Mobile and given Mobile ...
|
|
if (!request.Context.MobileNumber.IsNullOrEmpty() &&
|
|
mobileNumber != request.Context.MobileNumber)
|
|
{
|
|
XException.InvalidMobileNumber.Throw();
|
|
}
|
|
|
|
//
|
|
// Check User Added To Db or Not ...
|
|
var isAddedUser = !request.Context.UserId.IsNullOrEmpty();
|
|
if (isAddedUser)
|
|
{
|
|
//
|
|
// Validate User Exists and Retireve User Object ...
|
|
var user = await ValidateUserExistsAndRetrieve(
|
|
userSelectByParam,
|
|
containDetails: false,
|
|
checkCanLoginPolicies: false,
|
|
checkIsBanned: false);
|
|
|
|
//
|
|
// Check Mobiles Same ...
|
|
if (user.PhoneNumber == mobileNumber &&
|
|
user.PhoneNumberConfirmed)
|
|
{
|
|
XException.MobilesSame.Throw();
|
|
}
|
|
|
|
//
|
|
// Update User Object's Mobile Number and
|
|
// Confirmation Status ...
|
|
user.PhoneNumber = mobileNumber;
|
|
user.PhoneNumberConfirmed = false;
|
|
|
|
//
|
|
// Save Changes to Db ...
|
|
await UpdateUserAsync(user, checkCanLoginPolicies: false, checkIsBanned: false);
|
|
}
|
|
|
|
//
|
|
// Prepare Verification Request by Checking Next Tries,
|
|
// Device Status, and etc, Update or Add XVerificationRequest to db
|
|
// and Retrieve the added Entity ...
|
|
xVerificationRequest = await HandleVerificationRequestPreparation(verificationCode, request);
|
|
|
|
//
|
|
// Update Request Context MobileNumber
|
|
// and it's Confirmation Status ...
|
|
request.Context.MobileNumber = mobileNumber;
|
|
request.Context.MobileVerified = false;
|
|
|
|
//
|
|
// Remove Previous Token ...
|
|
await RemoveTokenByHash(actionToken);
|
|
|
|
//
|
|
// Prepare New Token Result Resource,
|
|
// by Requesting an Action ...
|
|
var result = await ActionRequest(
|
|
lang,
|
|
device,
|
|
userSelectByParam,
|
|
XAction.RequestMobileVerificationCode,
|
|
request.Context,
|
|
forceCheckUserExists: isAddedUser
|
|
);
|
|
|
|
//
|
|
// Prepare Message ...
|
|
var xMessage = GetVerificationCodeMessage(lang, mobileNumber, verificationCode);
|
|
|
|
//
|
|
// Send Verification Code Message to User Using SMS Provider ...
|
|
await MessageProvider.SendSmsAsync(xMessage, throwException: false);
|
|
|
|
//
|
|
// Return Result ...
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request a Email Verification Code
|
|
/// </summary>
|
|
/// <param name="lang">specify destination language</param>
|
|
/// <param name="device">an instance of <see>XDevice</see></param>
|
|
/// <param name="actionToken">a token which approved user action</param>
|
|
/// <param name="emailAddress">the email address which is going to request a verification code</param>
|
|
/// <param name="checkEmailInUse">check email address is in use or not, default is true</param>
|
|
/// <returns>an instance of <see>XActionResponse</see></returns>
|
|
public async Task<XActionResponse> RequestEmailVerificationCode(
|
|
string lang,
|
|
XDevice device,
|
|
string actionToken,
|
|
string emailAddress,
|
|
bool checkEmailInUse = true
|
|
)
|
|
{
|
|
//
|
|
// Validate Args ...
|
|
await ValidationProvider
|
|
.GroupValidationBuilder()
|
|
.AddNotEmpty(lang, actionToken, emailAddress)
|
|
.AddEmailAddress(emailAddress)
|
|
.AddNotNull(device)
|
|
.ValidateGroupAsync();
|
|
|
|
//
|
|
// Check Device Validation ...
|
|
await ValidateDeviceForActions(device);
|
|
|
|
//
|
|
// Check Email is Unique ...
|
|
if (checkEmailInUse)
|
|
{
|
|
var isUserExists = await IsUserExistsAsync(emailAddress);
|
|
if (isUserExists)
|
|
{
|
|
//
|
|
var xUser = await GetUserAsync(emailAddress);
|
|
|
|
//
|
|
if (xUser.EmailConfirmed)
|
|
{
|
|
XException.EmailInUsed.Throw();
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// Get Related Token to Registration Hash and Validate it,
|
|
// then Parse XActionRequest Instance from Token ...
|
|
var request = await ValidateAndParseActionHash(actionToken);
|
|
|
|
//
|
|
// Validate two Device Must Same ...
|
|
ValidateRequestAndGiveDevices(device, request.Context.Device);
|
|
|
|
//
|
|
// Extract UserSelectByParam from XActionRequest ...
|
|
var userSelectByParam = GetUserSelectByParam(
|
|
request,
|
|
forceNotNull: false,
|
|
excludes: new List<XUserSelectBy> {
|
|
XUserSelectBy.ID,
|
|
XUserSelectBy.Email,
|
|
XUserSelectBy.MobileNumber
|
|
});
|
|
|
|
//
|
|
// if it's null, means user doesn't have Invitation Token
|
|
// and there is no User Selector ...
|
|
if (userSelectByParam.IsNullOrEmpty())
|
|
{
|
|
//
|
|
// Since Email is Unique so it can act as a
|
|
// UserSelector ...
|
|
userSelectByParam = emailAddress;
|
|
}
|
|
|
|
//
|
|
// Define empty Verification Code
|
|
// and Empty Verification Request ...
|
|
var verificationCode = "";
|
|
XVerificationRequest xVerificationRequest = null;
|
|
|
|
//
|
|
// Check Device Request Verification Code before or Not ...
|
|
var isDeviceRequested = await IsDeviceRequestedForVerificationCodeBefor(device);
|
|
if (isDeviceRequested)
|
|
{
|
|
//
|
|
// if Requested before, Retrieve XVerification instance by Device from Db
|
|
// and Validate it ...
|
|
xVerificationRequest = await ValidateAndRetrieveVerificationRequest(device);
|
|
|
|
//
|
|
if (xVerificationRequest.VerificationCode.IsNullOrEmpty())
|
|
{
|
|
//
|
|
verificationCode = GenerateRandom().ToString();
|
|
xVerificationRequest.VerificationCode = verificationCode;
|
|
|
|
//
|
|
await RemoveVerificationCodeRequestByDevice(
|
|
device,
|
|
saveChanges: false
|
|
);
|
|
await AddVerificationRequest(
|
|
xVerificationRequest,
|
|
saveChanges: true
|
|
);
|
|
}
|
|
|
|
//
|
|
// if XVerificationRequest is Valid, it must Contain an Alive Verification Code
|
|
// so retrieve it and assign it ...
|
|
verificationCode = xVerificationRequest.VerificationCode;
|
|
}
|
|
else
|
|
{
|
|
//
|
|
verificationCode = GenerateRandom().ToString();
|
|
}
|
|
|
|
//
|
|
// Check Context Email and given Email ...
|
|
if (!request.Context.Email.IsNullOrEmpty() &&
|
|
emailAddress != request.Context.Email)
|
|
{
|
|
XException.InvalidEmailAddress.Throw();
|
|
}
|
|
|
|
//
|
|
// Check User Added To Db or Not ...
|
|
var isAddedUser = !request.Context.UserId.IsNullOrEmpty();
|
|
if (isAddedUser)
|
|
{
|
|
//
|
|
// Validate User Exists and Retireve User Object ...
|
|
var user = await ValidateUserExistsAndRetrieve(
|
|
userSelectByParam,
|
|
containDetails: false,
|
|
checkCanLoginPolicies: false,
|
|
checkIsBanned: false);
|
|
|
|
//
|
|
// Check Email Same ...
|
|
if (user.Email == emailAddress &&
|
|
user.EmailConfirmed)
|
|
{
|
|
XException.MobilesSame.Throw();
|
|
}
|
|
|
|
//
|
|
// Update User Object's Email and
|
|
// Confirmation Status ...
|
|
user.Email = emailAddress;
|
|
user.EmailConfirmed = false;
|
|
|
|
//
|
|
// Save Changes to Db ...
|
|
await UpdateUserAsync(
|
|
user,
|
|
checkCanLoginPolicies: false,
|
|
checkIsBanned: false
|
|
);
|
|
}
|
|
|
|
//
|
|
// Prepare Verification Request by Checking Next Tries,
|
|
// Device Status, and etc, Update or Add XVerificationRequest to db
|
|
// and Retrieve the added Entity ...
|
|
xVerificationRequest = await HandleVerificationRequestPreparation(verificationCode, request);
|
|
|
|
//
|
|
// Update Request Context Email
|
|
// and it's Confirmation Status ...
|
|
request.Context.Email = emailAddress;
|
|
request.Context.EmailVerified = false;
|
|
|
|
//
|
|
// Prepare New Token Result Resource,
|
|
// by Requesting an Action ...
|
|
var result = await ActionRequest(
|
|
lang,
|
|
device,
|
|
userSelectByParam,
|
|
XAction.RequestEmailVerificationCode,
|
|
request.Context,
|
|
forceCheckUserExists: isAddedUser
|
|
);
|
|
|
|
//
|
|
// Remove Hash ...
|
|
await RemoveTokenByHash(actionToken);
|
|
|
|
//
|
|
// Prepare Message ...
|
|
var xMessage = GetVerificationCodeMessage(lang, emailAddress, verificationCode);
|
|
|
|
//
|
|
// Send Verification Code Message to User Using Mail Provider ...
|
|
await MessageProvider.SendMailAsync(xMessage, throwException: false);
|
|
|
|
//
|
|
// Return Result ...
|
|
return result;
|
|
}
|
|
#endregion
|
|
}
|
|
} |