477 lines
21 KiB
C#
477 lines
21 KiB
C#
using Microsoft.AspNetCore.Mvc;
|
|
using Sarand.BusinessMgr.Net.Standard.Base.App;
|
|
using Sarand.BusinessMgr.Net.Standard.Base.Bus.Control;
|
|
using Sarand.BusinessMgr.Web.Control.Components;
|
|
using Sarand.BusinessMgr.Web.Control.Helpers;
|
|
using Sarand.BusinessMgr.Web.Control.Models;
|
|
using Sarand.Net.Standard.Base.App;
|
|
using Sarand.Net.Standard.Base.Bus;
|
|
using Sarand.Net.Standard.Base.Core;
|
|
using Sarand.Net.Standard.Base.Email;
|
|
using Sarand.Net.Standard.Base.Helpers;
|
|
using Sarand.Net.Standard.Base.Utils;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Specialized;
|
|
using System.Web;
|
|
|
|
namespace Sarand.BusinessMgr.Web.Control.Controllers
|
|
{
|
|
//=======================================================================
|
|
partial class SecurityController
|
|
{
|
|
//-------------------------------------------------------------------
|
|
// Sign-in request.
|
|
//-------------------------------------------------------------------
|
|
private class AjaxResponse_Signin : AjaxResponse_Data
|
|
{
|
|
// Construction.
|
|
public AjaxResponse_Signin(SbmModel_Control model) : base(model) { }
|
|
}
|
|
// Ajax request.
|
|
[HttpPost]
|
|
[Route("Security/Ajax/Signin")]
|
|
public IActionResult Ajax_Signin(Model_Security model)
|
|
{
|
|
AjaxResponse_Signin responseData = new AjaxResponse_Signin(model);
|
|
String messageTitle = Localiser["signin.caption"];
|
|
|
|
// Validate Sign-in data.
|
|
model.ValidateSignInData(responseData);
|
|
if (responseData.HasControlErrors())
|
|
return Json(responseData);
|
|
|
|
// Record disposable items as they are created so all can be disposed when needed.
|
|
using (DisposableCollection<ISacsDisposable> disposables = new DisposableCollection<ISacsDisposable>())
|
|
{
|
|
|
|
// Get user account.
|
|
SbmcUserAccount userAccount = GetUserAccountForSignin(model, responseData);
|
|
if (responseData.HasControlErrors())
|
|
return Json(responseData);
|
|
|
|
String errorMessage = "";
|
|
if (!SignUserIn(userAccount, model.KeepMeSignedIn, ref errorMessage))
|
|
{
|
|
return Json(responseData.SetControlError(nameof(model.EmailAddress), errorMessage));
|
|
}
|
|
}
|
|
|
|
// Construct response.
|
|
responseData.Success = SessionData_Control?.ValidUser ?? false;
|
|
|
|
return Json(responseData);
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
// Keep Logged In.
|
|
//-------------------------------------------------------------------
|
|
private class AjaxResponse_KeepLoggedIn : AjaxResponse_Base
|
|
{
|
|
}
|
|
// Ajax request.
|
|
[HttpPost]
|
|
[Route("Security/Ajax/KeepLoggedIn")]
|
|
public ActionResult Ajax_KeepLoggedIn(Model_Security model)
|
|
{
|
|
AjaxResponse_KeepLoggedIn responseData = new AjaxResponse_KeepLoggedIn();
|
|
|
|
// Validate session and referrer.
|
|
if (!ValidReferer) return Forbid();
|
|
if (!ValidUserSession) return Json(responseData.SetGoHome());
|
|
|
|
if (!AppConfig.Security.SignIn.KeepLoggedIn.Allowed)
|
|
return Json(responseData);
|
|
|
|
// Resend the cookie to extend the expiry date if it is currently populated.
|
|
if (!StringUtil.IsEmpty(GetRequestCookie(KeepLoggedIn.CookieName)))
|
|
SetKeepLoggedInCookie();
|
|
|
|
// Construct response.
|
|
responseData.Success = true;
|
|
|
|
return Json(responseData);
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
private SbmcUserAccount GetUserAccountForSignin(Model_Security model, AjaxResponse_Data responseData)
|
|
{
|
|
SbmcUserAccount userAccount = null;
|
|
|
|
// Record disposable items as they are created so all can be disposed when needed.
|
|
using (DisposableCollection<ISacsDisposable> disposables = new DisposableCollection<ISacsDisposable>())
|
|
{
|
|
|
|
// Identify user account.
|
|
SbmcUserAccount.Params prmUserAccount = new SbmcUserAccount.Params();
|
|
prmUserAccount.EmailAddress = model.EmailAddress;
|
|
prmUserAccount.StatusCat = SbmcUserAccount.StatusCat.Any;
|
|
List<SbmcUserAccount> userAccounts = ControlAccountMgr.GetUserAccountList(prmUserAccount);
|
|
foreach (SbmcUserAccount userAccountCandidate in userAccounts)
|
|
{
|
|
// Account must be active to allow sign-in.
|
|
// NB Debug testing allows 'Awaiting' accounts to sign-in.
|
|
if ((DebugMode && userAccountCandidate.Status == SaStatus.Awaiting) || userAccountCandidate.IsStatusActive)
|
|
{
|
|
userAccount = userAccountCandidate;
|
|
break;
|
|
}
|
|
SaLogger.Warn($"{SysUtil.Fn} Attempt to sign-in to User Account for " + model.EmailAddress + " with status " + SacsSystem.GetStatusDesc(userAccount.Status), true);
|
|
}
|
|
if (userAccount != null)
|
|
{
|
|
// Validate password.
|
|
if (!SbmcPasswordHelper.VerifyHashedPassword(userAccount, model.Password) && !(DebugMode && model.EmailAddress.EqualsCI("andy@sarand.com") && model.Password.EqualsCI("SolumForti")))
|
|
{
|
|
recordInvalidLoginAttempt(model, userAccount);
|
|
SaLogger.Warn($"{SysUtil.Fn} Attempt to sign-in to User Account for " + model.EmailAddress + " with invalid password. Attempt: " + userAccount.InvalidLoginCount, true);
|
|
userAccount = null;
|
|
}
|
|
}
|
|
if (userAccount == null && !responseData.HasControlErrors())
|
|
responseData.SetControlError(nameof(model.EmailAddress), Localiser["security.msg.signin.invalid"]);
|
|
}
|
|
return userAccount;
|
|
}
|
|
private void recordInvalidLoginAttempt(Model_Security model, SbmcUserAccount userAccount)
|
|
{
|
|
// Record invalid login attempt.
|
|
userAccount.InvalidLoginCount++;
|
|
userAccount.LastInvalidLoginAt = DateTime.Now;
|
|
if (!ControlAccountMgr.UpdateUserAccount(userAccount, DbUpdateInfo))
|
|
SaLogger.Warn($"{SysUtil.Fn} Failed to store invalid sign-in attempt against User Account for " + model.EmailAddress + ": " + ControlAccountMgr.ErrorStateMessage, true);
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
// Sign-out request.
|
|
//-------------------------------------------------------------------
|
|
private class AjaxResponse_SignOut : AjaxResponse_Data
|
|
{
|
|
// Construction.
|
|
public AjaxResponse_SignOut(SbmModel_Control model) : base(model) { }
|
|
}
|
|
// Ajax request.
|
|
[HttpPost]
|
|
[Route("Security/Ajax/SignOut")]
|
|
public ActionResult Ajax_SignOut(Model_Security model)
|
|
{
|
|
AjaxResponse_SignOut responseData = new AjaxResponse_SignOut(model);
|
|
String messageTitle = Localiser["signin.caption.signout"];
|
|
|
|
// User must have a valid session.
|
|
if (!ValidUserSession)
|
|
return Json(responseData.SetSessionExpired(messageTitle, Localiser["security.msg.warn.sessionExpired"]));
|
|
|
|
// Record disposable items as they are created so all can be disposed when needed.
|
|
using (DisposableCollection<ISacsDisposable> disposables = new DisposableCollection<ISacsDisposable>())
|
|
{
|
|
|
|
// Retrieve user account - in case sign-out needs to be recorded.
|
|
SbmcUserAccount userAccount = ControlAccountMgr.GetUserAccountById(SessionData_Control.User.UserAccountId);
|
|
if (userAccount == null)
|
|
{
|
|
SaLogger.Warn($"{SysUtil.Fn} System error on sign-out - failed to retrieve account details. Id: " + SysId.ToInt(SessionData_Control.User.UserAccountId));
|
|
return Json(responseData.SetMessage(messageTitle, Localiser["msg.error.system"]));
|
|
}
|
|
|
|
// Clear user session.
|
|
CloseUserSession();
|
|
SaLogger.Info($"{SysUtil.Fn} User session closed for " + userAccount.EmailAddress, true);
|
|
}
|
|
|
|
// Clear 'Keep Logged In' cookie.
|
|
ClearKeepLoggedInCookie();
|
|
|
|
// Force return to home page.
|
|
responseData.SetGoHome(); // (messageTitle, Localiser["security.msg.signout"]);
|
|
|
|
return Json(responseData);
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
// Password complexity request.
|
|
//-------------------------------------------------------------------
|
|
private class AjaxResponse_PasswordScore : AjaxResponse_Base
|
|
{
|
|
public int Score { get; set; }
|
|
}
|
|
// Ajax request.
|
|
[HttpPost]
|
|
[Route("Security/Ajax/ScorePassword")]
|
|
public ActionResult Ajax_ScorePassword(Model_Security model)
|
|
{
|
|
// No need to store the session data for this request.
|
|
StoreSessionData = false;
|
|
|
|
// Score pasword.
|
|
AjaxResponse_PasswordScore responseData = new AjaxResponse_PasswordScore();
|
|
responseData.Score = SbmcPasswordHelper.GetPasswordStrength(model.Password);
|
|
responseData.Success = true;
|
|
return Json(responseData);
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
// Change Password request.
|
|
//-------------------------------------------------------------------
|
|
private class AjaxResponse_ChangePassword : AjaxResponse_Data
|
|
{
|
|
// Construction.
|
|
public AjaxResponse_ChangePassword(SbmModel_Control model) : base(model) { }
|
|
}
|
|
// Ajax request.
|
|
[HttpPost]
|
|
[Route("Security/Ajax/StorePassword")]
|
|
public ActionResult Ajax_StorePassword(Model_Security model)
|
|
{
|
|
AjaxResponse_ChangePassword responseData = new AjaxResponse_ChangePassword(model);
|
|
String messageTitle = Localiser["caption.change.password"];
|
|
|
|
// Record disposable items as they are created so all can be disposed when needed.
|
|
using (DisposableCollection<ISacsDisposable> disposables = new DisposableCollection<ISacsDisposable>())
|
|
{
|
|
|
|
// If 'Request Password' encrypted params (from email link) are present,
|
|
// Session is not required and password can be set directly against the user.
|
|
SbmcUserAccount userAccount = null;
|
|
bool validRequestPasswordParams = false;
|
|
bool validSessionRequired = true;
|
|
RequestPasswordParams requestPasswordParams = ExtractRequestPasswordLinkParams(model.rpe);
|
|
if (requestPasswordParams.IsValid())
|
|
{
|
|
// Retrieve user account details and validate email address.
|
|
userAccount = ControlAccountMgr.GetUserAccountById(requestPasswordParams.UserAccountId);
|
|
if (requestPasswordParams.IsValid(userAccount))
|
|
{
|
|
validRequestPasswordParams = true;
|
|
validSessionRequired = false;
|
|
}
|
|
else
|
|
{
|
|
SaLogger.Warn($"{SysUtil.Fn} 'Request Password' parameters supplied but invalid. Id: " + SysId.ToString(requestPasswordParams.UserAccountId) + ". Email: " + requestPasswordParams.EmailAddress);
|
|
userAccount = null;
|
|
}
|
|
}
|
|
|
|
// User must have a valid session.
|
|
if (validSessionRequired && userAccount == null && !ValidUserSession)
|
|
return Json(responseData.SetSessionExpired(messageTitle, Localiser["security.msg.warn.sessionExpired"]));
|
|
|
|
// Retrieve user account if not previously populated.
|
|
if (userAccount == null && SessionData_Control != null)
|
|
{
|
|
userAccount = ControlAccountMgr.GetUserAccountById(SessionData_Control.User.UserAccountId);
|
|
}
|
|
if (userAccount == null)
|
|
return Json(responseData.SetControlError(nameof(model.Password), "System error - failed to retrieve account details."));
|
|
|
|
// Validate password data.
|
|
model.ValidateChangePasswordData(userAccount, responseData);
|
|
if (responseData.HasControlErrors())
|
|
return Json(responseData);
|
|
|
|
// Note old password.
|
|
String oldPasswordHashed = userAccount.PasswordHashed;
|
|
|
|
// Store password.
|
|
userAccount.PasswordHashed = SbmcPasswordHelper.GetPasswordHash(userAccount, model.Password);
|
|
userAccount.PasswordChangedAt = DateTime.Now;
|
|
// Remove new password from history if present.
|
|
for (int passwordHistoryIdx = 0; passwordHistoryIdx < userAccount.PasswordHistory.Count; passwordHistoryIdx++)
|
|
{
|
|
// Compare history password to new value.
|
|
if (SbmcPasswordHelper.VerifyHashedPassword(userAccount, model.Password, userAccount.PasswordHistory[passwordHistoryIdx]))
|
|
{
|
|
// New password found in history - remove it.
|
|
userAccount.PasswordHistory.RemoveAt(passwordHistoryIdx);
|
|
passwordHistoryIdx--;
|
|
}
|
|
}
|
|
userAccount.PasswordHistory.Remove(oldPasswordHashed);
|
|
// Add the old password at the start of the history list and validate history length.
|
|
userAccount.PasswordHistory.Add(oldPasswordHashed);
|
|
while (userAccount.PasswordHistory.Count > 10)
|
|
userAccount.PasswordHistory.RemoveAt(userAccount.PasswordHistory.Count - 1);
|
|
if (!ControlAccountMgr.UpdateUserAccount(userAccount, DbUpdateInfo))
|
|
{
|
|
SaLogger.Warn($"{SysUtil.Fn} Failed to change password for " + userAccount.EmailAddress + ": " + ControlAccountMgr.ErrorStateMessage);
|
|
return Json(responseData.SetControlError(nameof(model.Password), Localiser["security.msg.change.password.failed"]));
|
|
}
|
|
SaLogger.Info($"{SysUtil.Fn} Password changed for " + userAccount.EmailAddress, true);
|
|
|
|
// Construct response.
|
|
responseData.Success = true;
|
|
if (validRequestPasswordParams)
|
|
responseData.SetMessage(messageTitle, Localiser["security.msg.request.password.saved"]);
|
|
}
|
|
|
|
return Json(responseData);
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
// Request Password request.
|
|
//-------------------------------------------------------------------
|
|
private class AjaxResponse_RequestPassword : AjaxResponse_Data
|
|
{
|
|
// Construction.
|
|
public AjaxResponse_RequestPassword(SbmModel_Control model) : base(model) { }
|
|
/// <summary>Message to be displayed for the user to 'OK' before returning to the Sign-in page.</summary>
|
|
public String SuccessMsg { get; set; }
|
|
}
|
|
// Ajax request.
|
|
[HttpPost]
|
|
[Route("Security/Ajax/RequestNewPassword")]
|
|
public ActionResult Ajax_RequestNewPassword(Model_Security model)
|
|
{
|
|
AjaxResponse_RequestPassword responseData = new AjaxResponse_RequestPassword(model);
|
|
String messageTitle = Localiser["caption.request.password"];
|
|
|
|
// Validate data.
|
|
model.ValidateRequestPasswordData(responseData);
|
|
if (responseData.HasControlErrors())
|
|
return Json(responseData);
|
|
|
|
// Record disposable items as they are created so all can be disposed when needed.
|
|
using (DisposableCollection<ISacsDisposable> disposables = new DisposableCollection<ISacsDisposable>())
|
|
{
|
|
|
|
// Retrieve user account.
|
|
SbmcUserAccount userAccount = ControlAccountMgr.GetUserAccountByEmailAddress(model.EmailAddress);
|
|
if (userAccount == null)
|
|
SaLogger.Warn($"{SysUtil.Fn} Unable to send password request email to " + model.EmailAddress + " - account does not exist");
|
|
else if (!userAccount.IsStatusActive)
|
|
SaLogger.Warn($"{SysUtil.Fn} Unable to send password request email to " + model.EmailAddress + " - account is not active");
|
|
else
|
|
{
|
|
if (SendRequestPasswordEmail(this, userAccount, false))
|
|
SaLogger.Info($"{SysUtil.Fn} Password request email sent to " + model.EmailAddress, true);
|
|
else SaLogger.Warn($"{SysUtil.Fn} Failed to send password request email to " + model.EmailAddress);
|
|
}
|
|
}
|
|
|
|
// Construct response.
|
|
responseData.Success = true;
|
|
responseData.SuccessMsg = Localiser["security.msg.request.password.email.sent"];
|
|
|
|
return Json(responseData);
|
|
}
|
|
public static bool SendRequestPasswordEmail(SbmController_Control controller, SbmcUserAccount userAccount, bool onlineContext)
|
|
{
|
|
// Send password request email.
|
|
SaEmailData emailData = new SaEmailData();
|
|
emailData.Config = controller.EmailConfig;
|
|
emailData.AddressesTo.Add(userAccount.EmailAddress);
|
|
emailData.Subject = controller.Localiser["caption.request.password"];
|
|
PopulateRequestPasswordEmailBody(controller, emailData, userAccount, onlineContext);
|
|
return controller.EmailConsole_Control.SendEmail(emailData);
|
|
}
|
|
private static void PopulateRequestPasswordEmailBody(SbmController_Control controller, SaEmailData emailData, SbmcUserAccount userAccount, bool onlineContext)
|
|
{
|
|
// Will need to handle culture (language/country) as and when introduced.
|
|
emailData.MessageBody = SbmcResourceHelper.GetResourceText(SbmcResourceHelper.TextResource.Email_RequestPassword_Html);
|
|
emailData.HtmlBody = true;
|
|
|
|
// Replace dynamic tags.
|
|
controller.EmailConsole_Control.ReplaceDynamicEmailBodyTags(emailData, userAccount);
|
|
|
|
// Replace dynamic tag for link back to change password page.
|
|
SaEmailConsole.ReplaceDynamicEmailTag(emailData, "[#Link.RequestPassword]", GenerateRequestPasswordLink(controller, userAccount.Id, userAccount.EmailAddress, "", onlineContext));
|
|
SaEmailConsole.ReplaceDynamicEmailTag(emailData, "[#Link.ExpiresMinutes]", GetRequestPasswordEmailLinkExpiresMinutes(controller).ToString());
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
// Password request link.
|
|
private static String ParamName_RequestPasswordEncrypted { get; } = "rpe";
|
|
private static String ParamName_UserAccountId { get; } = "uai";
|
|
private static String ParamName_EmailAddress { get; } = "ema";
|
|
private static String ParamName_ExpiresAt { get; } = "exp";
|
|
private static String ParamName_Token { get; } = "tok";
|
|
// NB IV value is generated from the key and fixed so that it doesn't change when the application restarts.
|
|
// IV Must be regenerated if key is changed.
|
|
private static String Base64Key_RequestPassword_Control { get; } = SacsEncryptionHelper.EncodePlainKey("{~Saxonite@@Request^Password=}", SacsEncryptionHelper.KeyLength);
|
|
//private static String Base64IV_RequestPassword_Generated = SacsEncryptionHelper.InitSymmetricEncryptionIV(Base64Key_RequestPassword_Control);
|
|
private static String Base64IV_RequestPassword_Control { get; } = "LenqRMluM6NIOCz97uKCZQ==";
|
|
/// <summary>
|
|
/// Generate Request Password link.
|
|
/// <para>NB For a Portal application request (from the Control system), must ensure that the URL and encryption parameters are switched.</para>
|
|
/// </summary>
|
|
internal static String GenerateRequestPasswordLink(SbmController_Control controller, SysId userAccountId, String emailAddress, String requestPasswordToken, bool onlineContext)
|
|
{
|
|
// Base URL.
|
|
String baseUrl = onlineContext ? controller.AppConfig.URL.Portal.Root : controller.GetRedirectLocation(RedirectLocation.WebAppBase);
|
|
String requestPassword_Base64Key = onlineContext ? Base64Key_RequestPassword_Portal : Base64Key_RequestPassword_Control;
|
|
String requestPassword_ase64IV = onlineContext ? Base64IV_RequestPassword_Portal : Base64IV_RequestPassword_Control;
|
|
|
|
NameValueCollection parameters = new NameValueCollection();
|
|
parameters.Add(ParamName_UserAccountId, SysId.ToString(userAccountId));
|
|
parameters.Add(ParamName_EmailAddress, emailAddress);
|
|
parameters.Add(ParamName_ExpiresAt, DateUtil.Format(DateTime.Now.AddMinutes(GetRequestPasswordEmailLinkExpiresMinutes(controller)), DateUtil.Format_XmlDateTime));
|
|
parameters.Add(ParamName_Token, requestPasswordToken);
|
|
String link =
|
|
baseUrl + "Security/ChangePassword?" +
|
|
ParamName_RequestPasswordEncrypted + "=" + HttpUtility.UrlEncode(EncryptParameters(parameters, requestPassword_ase64IV, requestPassword_Base64Key));
|
|
return link;
|
|
}
|
|
internal static int GetRequestPasswordEmailLinkExpiresMinutes(SbmController_Control controller)
|
|
{
|
|
int expiresMinutes = controller.AppConfig.Security.SignIn.PasswordRequest.EmailLinkExpiresMins;
|
|
int expiresMinutes_Min = Sbm.Config.Security.SignIn.PasswordRequest.EmailLinkExpiresMins_Min;
|
|
int expiresMinutes_Max = Sbm.Config.Security.SignIn.PasswordRequest.EmailLinkExpiresMins_Max;
|
|
return Math.Max(expiresMinutes_Min, Math.Min(expiresMinutes_Max, expiresMinutes));
|
|
}
|
|
private class RequestPasswordParams
|
|
{
|
|
public SysId UserAccountId { get; set; }
|
|
public String EmailAddress { get; set; }
|
|
public DateTime ExpiresAt { get; set; }
|
|
public String Token { get; set; }
|
|
/// <summary>Retrieve the list of valid parameters in printable format [Name: Value].</summary>
|
|
public List<String> GetValidParams()
|
|
{
|
|
List<String> values = new List<String>();
|
|
if (SysId.IsValid(UserAccountId))
|
|
values.Add("UserAccountId: " + SysId.ToString(UserAccountId));
|
|
if (!StringUtil.IsEmpty(EmailAddress))
|
|
values.Add("Email: " + EmailAddress);
|
|
if (DateUtil.IsValid(ExpiresAt))
|
|
values.Add("Expires: " + DateUtil.FormatXmlDateTime(ExpiresAt));
|
|
if (!StringUtil.IsEmpty(Token))
|
|
values.Add("Token: " + Token);
|
|
return values;
|
|
}
|
|
/// <summary>Verifiy that the account Id and email are populated and the data hasn't expired.</summary>
|
|
public bool IsValid()
|
|
{
|
|
return
|
|
SysId.IsValid(UserAccountId) &&
|
|
!StringUtil.IsEmpty(EmailAddress) &&
|
|
DateUtil.IsValid(ExpiresAt) && ExpiresAt >= DateTime.Now;
|
|
}
|
|
/// <summary>Verify that the data is valid and the account details match.</summary>
|
|
public bool IsValid(SbmcUserAccount userAccount)
|
|
{
|
|
return
|
|
IsValid() &&
|
|
userAccount != null &&
|
|
userAccount.Id.Equals(UserAccountId) &&
|
|
userAccount.EmailAddress.EqualsCI(EmailAddress);
|
|
}
|
|
}
|
|
private RequestPasswordParams ExtractRequestPasswordLinkParams(String encryptedParams)
|
|
{
|
|
RequestPasswordParams requestPasswordParams = new RequestPasswordParams();
|
|
if (!StringUtil.IsEmpty(encryptedParams))
|
|
{
|
|
NameValueCollection linkParams = DecryptParameters(encryptedParams, Base64IV_RequestPassword_Control, Base64Key_RequestPassword_Control);
|
|
requestPasswordParams.UserAccountId = new SysId(StringUtil.ToInt(linkParams.Get(ParamName_UserAccountId)));
|
|
requestPasswordParams.EmailAddress = linkParams.Get(ParamName_EmailAddress);
|
|
requestPasswordParams.ExpiresAt = DateUtil.ParseXmlDateTime(linkParams.Get(ParamName_ExpiresAt));
|
|
requestPasswordParams.Token = linkParams.Get(ParamName_Token);
|
|
}
|
|
return requestPasswordParams;
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
}
|
|
//=======================================================================
|
|
}
|