Add project files.

This commit is contained in:
2021-05-11 19:38:43 +01:00
parent d316c1898d
commit ae64e14da4
1401 changed files with 283421 additions and 0 deletions
@@ -0,0 +1,152 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Saxonite.Net.Core.Web.Online.Models;
using Saxonite.Net.Core.Web.Base.App;
using Saxonite.Net.Core.Web.Base.Controllers;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Saxonite.Net.Standard.Base.Bus;
using Sarand.Net.Standard.Base.App;
using Sarand.Net.Standard.Base.Bus;
using Sarand.Net.Standard.Base.Utils;
namespace Saxonite.Net.Core.Web.Online.Controllers
{
//=======================================================================
// Online Account Controller - Ajax actions.
//=======================================================================
partial class AccountController
{
//-------------------------------------------------------------------
// Account - Get Contact Details.
//-------------------------------------------------------------------
public class ContactResponseData
{
public ContactResponseData(SaxUserAccount userAccount)
{
EmailAddress = userAccount.EmailAddress;
DisplayName = userAccount.GetDisplayName();
Title = userAccount.Title;
FirstName = userAccount.FirstName;
LastName = userAccount.LastName;
}
public String EmailAddress { get; }
public String DisplayName { get; }
public String Title { get; }
public String FirstName { get; }
public String LastName { get; }
}
public class AjaxResponse_AccountContactData : AjaxResponse_Data
{
public ContactResponseData Contact { get; set; }
}
// Ajax request.
[HttpPost]
[Route("Account/Ajax/GetProfileContact")]
public IActionResult GetProfileContact(Model_Account model)
{
String fn = GetType().Name + ".GetProfileContact";
AjaxResponse_AccountContactData responseData = new AjaxResponse_AccountContactData();
String messageTitle = Localiser["account.caption.contact"];
// Validate session and referrer.
if (!ValidReferer)
return Forbid();
if (!ValidUserSession)
return GetAjaxResult_InvalidSession(responseData);
// Validate data.
model.ValidateContactDetails(responseData);
if (responseData.HasControlErrors())
return Json(responseData);
// Retrieve user details.
SaxUserAccount userAccount = AccountMgr.GetUserAccountById(SessionData.User.UserAccountId);
if (userAccount == null)
{
SaLogger.Warn(fn + " Failed to retrieve user account for Id: " + SysId.ToString(SessionData.User.UserAccountId));
return Json(responseData.SetGoHome(messageTitle, @Localiser.GetJavascriptText("msg.something.gone.wrong")));
}
// Populate data to response.
responseData.Contact = new ContactResponseData(userAccount);
responseData.Success = true;
return Json(responseData);
}
//-------------------------------------------------------------------
// Account - Store Contact Details.
//-------------------------------------------------------------------
public class AjaxResponse_StoreAccountContactDetails : AjaxResponse_Data
{
public ContactResponseData Contact { get; set; }
}
// Ajax request.
[HttpPost]
[Route("Account/Ajax/StoreProfileContact")]
public IActionResult StoreProfileContact(Model_Account model)
{
String fn = GetType().Name + ".StoreProfileContact";
AjaxResponse_StoreAccountContactDetails responseData = new AjaxResponse_StoreAccountContactDetails();
String messageTitle = Localiser["account.caption.contact"];
// Validate session and referrer.
if (!ValidReferer)
return Forbid();
if (!ValidUserSession)
return GetAjaxResult_InvalidSession(responseData);
// Validate data.
model.ValidateContactDetails(responseData);
if (responseData.HasControlErrors())
return Json(responseData);
// Retrieve user details.
SaxUserAccount userAccount = AccountMgr.GetUserAccountById(SessionData.User.UserAccountId);
if (userAccount == null)
{
SaLogger.Warn(fn + " Failed to retrieve user account for Id: " + SysId.ToString(SessionData.User.UserAccountId));
return Json(responseData.SetGoHome(messageTitle, @Localiser.GetJavascriptText("msg.something.gone.wrong")));
}
if (!userAccount.EmailAddress.EqualsCI(SessionData.User.EmailAddress))
{
SaLogger.Warn(fn + " Unable to update user account '" + userAccount.EmailAddress + ". Email doesn't match session: " + SessionData.User.EmailAddress);
return Json(responseData.SetGoHome(messageTitle, @Localiser.GetJavascriptText("msg.something.gone.wrong")));
}
// Populate user properties.
userAccount.Changed = false;
if (model.AccountTitle != null)
userAccount.Title = model.AccountTitle;
if (model.AccountFirstName != null)
userAccount.FirstName = model.AccountFirstName;
if (model.AccountLastName != null)
userAccount.LastName = model.AccountLastName;
// Update User Account.
if (userAccount.Changed) SaLogger.Info(fn + " Updating user account '" + userAccount.EmailAddress + "'. Id: " + SysId.ToString(userAccount.Id), true);
if (AccountMgr.UpdateUserAccount(userAccount, DbUpdateInfo))
{
// ToDo Refresh Session with updated organisation details.
}
else
{
SaLogger.Warn(fn + " Failed to update user account '" + userAccount.EmailAddress + "': " + AccountMgr.ErrorStateMessage);
return Json(responseData.SetMessage(messageTitle, Localiser["msg.error.item.update.failed"]));
}
// Construct response.
responseData.Contact = new ContactResponseData(userAccount);
responseData.Success = true;
return Json(responseData);
}
//-------------------------------------------------------------------
}
//=======================================================================
}
@@ -0,0 +1,53 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Saxonite.Net.Core.Web.Online.Models;
using Saxonite.Net.Core.Web.Base.App;
using Saxonite.Net.Core.Web.Base.Controllers;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
namespace Saxonite.Net.Core.Web.Online.Controllers
{
//=======================================================================
// Online Account Controller.
//=======================================================================
public partial class AccountController : SaxController_Online
{
//-------------------------------------------------------------------
// Construction.
public AccountController(SaxLocaliser localiser, ILogger<HomeController> logger) :
base(localiser, logger)
{
}
//-------------------------------------------------------------------
[HttpGet]
public IActionResult Index()
{
// Validate session.
if (!ValidUserSession)
return RedirectToAction("", "");
Model_Account model = new Model_Account(Localiser);
return View(model);
}
//-------------------------------------------------------------------
[HttpGet]
public IActionResult Profile()
{
// Validate session.
if (!ValidUserSession)
return RedirectToAction("", "");
Model_Account model = new Model_Account(Localiser);
return View(model);
}
//-------------------------------------------------------------------
}
//=======================================================================
}
@@ -0,0 +1,93 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Saxonite.Net.Core.Web.Online.Models;
using Saxonite.Net.Core.Web.Base.App;
using Saxonite.Net.Core.Web.Base.Controllers;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Sarand.Net.Standard.Base.Bus;
using Saxonite.Net.Standard.Base.Bus;
using Sarand.Net.Standard.Base.App;
using Sarand.Net.Standard.Base.Utils;
namespace Saxonite.Net.Core.Web.Online.Controllers
{
//=======================================================================
public class HomeController : SaxController_Online
{
//-------------------------------------------------------------------
// Data members.
//-------------------------------------------------------------------
// Construction.
public HomeController(SaxLocaliser localiser, ILogger<HomeController> logger) :
base(localiser, logger)
{
}
//-------------------------------------------------------------------
[HttpGet]
public IActionResult Index()
{
String fn = GetType().Name + ".Index";
// Check for 'Keep Logged In' cookie.
// NB Only allowed if there isn't a current user session.
if (!ValidUserSession && SessionConfig.Security.SignIn.KeepLoggedIn.Allowed)
{
String encryptedParams = GetRequestCookie(CookieName_KeepLoggedIn);
KeepLoggedInParams keepLoggedInParams = ExtractKeepLoggedInParams(encryptedParams);
String logSuffix = keepLoggedInParams.GetValidParams().Concat("", " ", ".");
if (SysId.IsValid(keepLoggedInParams.UserAccountId))
{
SaxUserAccount userAccount = AccountMgr.GetUserAccountById(keepLoggedInParams.UserAccountId);
if (userAccount != null)
{
if (userAccount.IsStatusActive)
{
String errorMessage = "";
if (SignUserIn(userAccount, AccountMgr.GetRolesForUser(userAccount.Id), true, ref errorMessage))
SaLogger.Info(fn + " Keep Logged In cookie found." + logSuffix);
else SaLogger.Warn(fn + " Keep Logged In cookie found but failed to sign user in: " + errorMessage + "." + logSuffix);
}
else SaLogger.Warn(fn + " Keep Logged In cookie found but UserAccount is not active." + logSuffix);
}
else SaLogger.Warn(fn + " Keep Logged In cookie found but UserAccount not found." + logSuffix);
}
}
// Redirect to sign-in if not signed-in.
if (!ValidUserSession)
{
ClearKeepLoggedInCookie();
//return RedirectToAction("", "Security");
}
// Home page.
Model_Home model = new Model_Home(Localiser);
model.ProhibitCache = false;
return View(model);
}
//-------------------------------------------------------------------
[HttpGet]
public IActionResult Privacy()
{
Model_Home model = new Model_Home(Localiser);
return View(model);
}
//-------------------------------------------------------------------
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
//-------------------------------------------------------------------
}
//=======================================================================
}
@@ -0,0 +1,283 @@
using Microsoft.AspNetCore.Mvc;
using Sarand.Net.Standard.Base.App;
using Sarand.Net.Standard.Base.Bus;
using Sarand.Net.Standard.Base.Core;
using Sarand.Net.Standard.Base.Dbas;
using Sarand.Net.Standard.Base.Utils;
using Saxonite.Net.Core.Web.Online.Models;
using Saxonite.Net.Standard.Base.App;
using Saxonite.Net.Standard.Base.Bus;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Saxonite.Net.Core.Web.Online.Controllers
{
//=======================================================================
// Online Journalist Controller - Ajax actions.
//=======================================================================
partial class JournalistController
{
//-------------------------------------------------------------------
// Journalist Article data.
public class ArticleData_List
{
public String Id { get; set; }
public String Headline { get; set; }
public String Summary { get; set; }
public String RegionCaptionPrimary { get; set; }
public String SportCaptionPrimary { get; set; }
public String PublishedOnFmt { get; set; }
public String CreatedOnFmt { get; set; }
public String StatusCode { get; set; }
public String StatusDesc { get; set; }
}
public class ArticleData_Full : ArticleData_List
{
public String RegionIdPrimary { get; set; }
public String SportIdPrimary { get; set; }
public String PublishedOn_YYYYMMDD { get; set; }
}
private void PopulateArticleResponseData(Model_Journalist model, SaxJournalistArticle article, ArticleData_List data)
{
data.Id = SysId.ToString(article.Id);
data.Headline = article.Headline;
data.Summary = article.Summary;
data.RegionCaptionPrimary = model.GetRegionCaption(article.RegionIdPrimary);
data.SportCaptionPrimary = model.GetSportCaption(article.SportIdPrimary);
data.CreatedOnFmt = DateUtil.Format(article.CreateStamp.On, "d-MMM-yyyy");
data.PublishedOnFmt = DateUtil.Format(article.PublishedAt, "d-MMM-yyyy");
data.StatusCode = article.StatusCode;
data.StatusDesc = article.StatusDesc;
}
private void PopulateArticleResponseData(Model_Journalist model, SaxJournalistArticle article, ArticleData_Full data)
{
PopulateArticleResponseData(model, article, data as ArticleData_List);
data.RegionIdPrimary = SysId.ToString(article.RegionIdPrimary);
data.SportIdPrimary = SysId.ToString(article.SportIdPrimary);
data.PublishedOn_YYYYMMDD = DateUtil.Format(article.PublishedAt, "yyyyMMdd");
}
//-------------------------------------------------------------------
// Get Article List ajax request.
//-------------------------------------------------------------------
public class AjaxResponse_ArticleList : AjaxResponse_Data
{
public List<ArticleData_List> Articles { get; } = new List<ArticleData_List>();
}
//-------------------------------------------------------------------
[HttpPost]
[Route("Journalist/Ajax/GetArticleList")]
public IActionResult Ajax_GetArticleList(Model_Journalist model)
{
String fn = GetType().Name + ".Ajax_GetArticleList";
String messageTitle = Localiser["journalist.caption.articles"];
AjaxResponse_ArticleList responseData = new AjaxResponse_ArticleList();
// Validate session and referrer.
if (!ValidReferer)
return Forbid();
if (!ValidUserSession)
return GetAjaxResult_InvalidSession(responseData);
if (!UserIsJournalist)
return GetAjaxResult_GoHome(responseData);
// Item filter.
SaxJournalistArticle.Params prmArticle = new SaxJournalistArticle.Params();
prmArticle.UserAccountIdsJournalistIncluded.Add(SessionData.User.UserAccountId);
prmArticle.ArticleFilter = model.Filter.Article;
prmArticle.StatusCat = SaxJournalistArticle.StatusCat.NotDeleted;
model.DataItemTotal = (ulong)ActivityMgr.GetJournalistArticleCount(prmArticle);
model.PopulatePagingTo(prmArticle.ResultsPaging);
// Item columns.
ColumnDfn colId = ActivityMgr.GetJournalistArticleColumnDfn(SaxJournalistArticle.Column.Id);
ColumnDfn colHeadline = ActivityMgr.GetJournalistArticleColumnDfn(SaxJournalistArticle.Column.Headline);
ColumnDfn colSummary = ActivityMgr.GetJournalistArticleColumnDfn(SaxJournalistArticle.Column.Summary);
ColumnDfn colRegionIdPrimary = ActivityMgr.GetJournalistArticleColumnDfn(SaxJournalistArticle.Column.RegionIdPrimary);
ColumnDfn colSportIdPrimary = ActivityMgr.GetJournalistArticleColumnDfn(SaxJournalistArticle.Column.SportIdPrimary);
ColumnDfn colPublishedOn = ActivityMgr.GetJournalistArticleColumnDfn(SaxJournalistArticle.Column.PublishedAt_Date);
ColumnDfn colStatus = ActivityMgr.GetJournalistArticleColumnDfn(SaxJournalistArticle.Column.Status);
ColumnDfn colCreatedOn = ActivityMgr.GetJournalistArticleColumnDfn(SaxJournalistArticle.Column.CreateOn_Date);
List<ColumnDfn> columns = new List<ColumnDfn>()
{
colId, colHeadline, colSummary,
colRegionIdPrimary, colSportIdPrimary, colPublishedOn,
colStatus, colCreatedOn
};
// Sort columns.
colCreatedOn.SortAscending = false;
prmArticle.SortColumns.Add(colCreatedOn);
// Query items.
model.DataItemCount = 0;
using (DbResultSet resultSet = ActivityMgr.ExecuteJournalistArticleQuery(columns, prmArticle))
{
while (resultSet != null && resultSet.HasRows && resultSet.NextRecord())
{
model.DataItemCount++;
NamedValue.Map values = resultSet.GetRecordValues();
ArticleData_List articleData = new ArticleData_List()
{
Id = values.GetString(colId.DataName),
Headline = values.GetString(colHeadline.DataName),
Summary = values.GetString(colSummary.DataName),
RegionCaptionPrimary = model.GetRegionCaption(values.GetString(colRegionIdPrimary.DataName).ToSysId()),
SportCaptionPrimary = model.GetSportCaption(values.GetString(colSportIdPrimary.DataName).ToSysId()),
CreatedOnFmt = DateUtil.Format(values.GetDateTime(colCreatedOn.DataName), "d-MMM-yyyy"),
PublishedOnFmt = DateUtil.Format(values.GetDateTime(colPublishedOn.DataName), "d-MMM-yyyy"),
StatusCode = values.GetString(colStatus.DataName),
StatusDesc = SacsSystem.GetStatusDescForCode(values.GetString(colStatus.DataName))
};
responseData.Articles.Add(articleData);
}
}
// Complete response.
responseData.SetPaging(model);
responseData.Success = true;
return Json(responseData);
}
//-------------------------------------------------------------------
// Get Article ajax request.
//-------------------------------------------------------------------
public class AjaxResponse_Article : AjaxResponse_Data
{
public ArticleData_Full Article { get; } = new ArticleData_Full();
}
//-------------------------------------------------------------------
[HttpPost]
[Route("Journalist/Ajax/GetArticle")]
public IActionResult Ajax_GetArticle(Model_Journalist model)
{
String fn = GetType().Name + ".Ajax_GetArticle";
String messageTitle = Localiser["journalist.caption.article"];
AjaxResponse_Article responseData = new AjaxResponse_Article();
// Validate session and referrer.
if (!ValidReferer)
return Forbid();
if (!ValidUserSession)
return GetAjaxResult_InvalidSession(responseData);
if (!UserIsJournalist)
return GetAjaxResult_GoHome(responseData);
// Identify item.
SysId articleId = model.ArticleId.ToSysId();
if (!SysId.IsValid(articleId))
return Json(responseData.SetMessage(messageTitle, Localiser["msg.error.item.id.not.specified"]));
// Retrieve item.
SaxJournalistArticle article = ActivityMgr.GetJournalistArticleById(articleId);
if (article == null)
{
SaLogger.Warn(fn + " Failed to retrieve Journalist Article for Id: '" + model.ArticleId + "'");
return Json(responseData.SetMessage(messageTitle, Localiser["msg.error.item.id.not.found"]));
}
// Construct response.
PopulateArticleResponseData(model, article, responseData.Article);
// Complete response.
responseData.Success = true;
return Json(responseData);
}
//-------------------------------------------------------------------
// Store Article ajax request.
//-------------------------------------------------------------------
[HttpPost]
[Route("JOurnalist/Ajax/StoreArticle")]
public IActionResult Ajax_StoreArticle(Model_Journalist model)
{
String fn = GetType().Name + ".Ajax_StoreArticle";
String messageTitle = Localiser["journalist.caption.article"];
AjaxResponse_Article responseData = new AjaxResponse_Article();
// Validate session and referrer.
if (!ValidReferer)
return Forbid();
if (!ValidUserSession)
return GetAjaxResult_InvalidSession(responseData);
if (!UserIsJournalist)
return GetAjaxResult_GoHome(responseData);
// Validate item properties.
model.ValidateArticleData(responseData);
if (responseData.HasControlErrors())
return Json(responseData);
// Validate Article against Id against 'Exists' flag.
SysId articleId = model.ArticleId.ToSysId();
bool articleExists = StringUtil.ToBool(model.ArticleExists);
if (articleExists && !SysId.IsValid(articleId))
{
SaLogger.Warn(fn + " Id not specified for existing Article.");
return Json(responseData.SetGoHome(messageTitle, Localiser["msg.error.system"]));
}
if (!articleExists && SysId.IsValid(articleId))
{
SaLogger.Warn(fn + " Id specified for new Article: '" + model.ArticleId + "'");
return Json(responseData.SetGoHome(messageTitle, Localiser["msg.error.system"]));
}
// Retrieve existing item.
SaxJournalistArticle article = ActivityMgr.GetJournalistArticleById(articleId);
if (article == null && SysId.IsValid(articleId))
{
SaLogger.Warn(fn + " Failed to retrieve article for Id: '" + model.ArticleId + "'");
return Json(responseData.SetMessage(messageTitle, Localiser["msg.error.item.id.not.found"]));
}
// Create new item if required.
if (article == null)
article = ActivityMgr.CreateJournalistArticle(SessionData.User.UserAccountId);
// Populate item properties.
if (model.ArticleRegionId != null)
article.RegionIdPrimary = model.ArticleRegionId.ToSysId();
if (model.ArticleSportId != null)
article.SportIdPrimary = model.ArticleSportId.ToSysId();
if (model.ArticleHeadline != null)
article.Headline = model.ArticleHeadline;
if (model.ArticleSummary != null)
article.Summary = model.ArticleSummary;
if (model.ArticleStatusCode != null)
article.Status = model.ArticleStatus;
// Store item.
if (!SysId.IsValid(article.Id))
{
if (!ActivityMgr.InsertJournalistArticle(article, DbUpdateInfo))
{
SaLogger.Warn(fn + " Failed to insert article: " + ActivityMgr.ErrorStateMessage);
return Json(responseData.SetMessage(messageTitle, Localiser["msg.error.item.insert.failed"] + "\n\n" + ActivityMgr.ErrorStateMessage));
}
}
else
{
if (!ActivityMgr.UpdateJournalistArticle(article, DbUpdateInfo))
{
SaLogger.Warn(fn + " Failed to update article [Id:" + SysId.ToString(article.Id) + "]: " + ActivityMgr.ErrorStateMessage);
return Json(responseData.SetMessage(messageTitle, Localiser["msg.error.item.update.failed"] + "\n\n" + ActivityMgr.ErrorStateMessage));
}
}
// Construct response.
PopulateArticleResponseData(model, article, responseData.Article);
// Complete response.
responseData.Success = true;
return Json(responseData);
}
//-------------------------------------------------------------------
}
//=======================================================================
}
@@ -0,0 +1,53 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Saxonite.Net.Core.Web.Base.App;
using Saxonite.Net.Core.Web.Online.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Saxonite.Net.Core.Web.Online.Controllers
{
//=======================================================================
// Online Journalist Controller.
//=======================================================================
public partial class JournalistController : SaxController_Online
{
//-------------------------------------------------------------------
// Construction.
public JournalistController(SaxLocaliser localiser, ILogger<HomeController> logger) :
base(localiser, logger)
{
}
//-------------------------------------------------------------------
[HttpGet]
public IActionResult Index()
{
// Validate session.
if (!ValidUserSession || !UserIsJournalist)
return RedirectToAction("", "");
// No 'Index' view as yet - default to 'Articles' view.
//Model_Journalist model = new Model_Journalist(Localiser);
//return View(model);
return RedirectToAction("Articles", "Journalist");
}
//-------------------------------------------------------------------
[HttpGet]
public IActionResult Articles()
{
// Validate session.
if (!ValidUserSession || !UserIsJournalist)
return RedirectToAction("", "");
Model_Journalist model = new Model_Journalist(Localiser);
return View(model);
}
//-------------------------------------------------------------------
}
//=======================================================================
}
@@ -0,0 +1,43 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Saxonite.Net.Core.Web.Online.Models;
using Saxonite.Net.Core.Web.Base.App;
using Saxonite.Net.Core.Web.Base.Controllers;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
namespace Saxonite.Net.Core.Web.Online.Controllers
{
//=======================================================================
public class PoliciesController : SaxController_Online
{
//-------------------------------------------------------------------
// Construction.
public PoliciesController(SaxLocaliser localiser, ILogger<HomeController> logger) :
base(localiser, logger)
{
}
//-------------------------------------------------------------------
[HttpGet]
public IActionResult Index()
{
Model_Policies model = new Model_Policies(Localiser);
return View(model);
}
//-------------------------------------------------------------------
[HttpGet]
public IActionResult Privacy()
{
Model_Policies model = new Model_Policies(Localiser);
return View(model);
}
//-------------------------------------------------------------------
}
//=======================================================================
}
@@ -0,0 +1,96 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Sarand.Net.Standard.Base.App;
using Sarand.Net.Standard.Base.Bus;
using Sarand.Net.Standard.Base.Core;
using Sarand.Net.Standard.Base.Dbas;
using Saxonite.Net.Core.Web.Base.App;
using Saxonite.Net.Core.Web.Online.Models;
using Saxonite.Net.Standard.Base.App;
using Saxonite.Net.Standard.Base.Bus;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Saxonite.Net.Core.Web.Online.Controllers
{
//=======================================================================
// Online Reference Controller - Ajax actions.
//=======================================================================
public partial class ReferenceController : SaxController_Online
{
//-------------------------------------------------------------------
// Construction.
public ReferenceController(SaxLocaliser localiser, ILogger<HomeController> logger) :
base(localiser, logger)
{}
//-------------------------------------------------------------------
// Regions
//-------------------------------------------------------------------
public class AjaxResponse_RegionList : AjaxResponse_Base
{
public List<SaxModel_Online.RegionData> Regions { get; } = new List<SaxModel_Online.RegionData>();
}
//-------------------------------------------------------------------
[HttpPost]
[Route("Reference/Ajax/GetRegions")]
public IActionResult Ajax_GetRegionList(Model_Reference model)
{
String fn = GetType().Name + ".Ajax_GetRegionList";
String messageTitle = Localiser["reference.caption.regions"];
AjaxResponse_RegionList responseData = new AjaxResponse_RegionList();
// Validate session and referrer.
if (!ValidReferer)
return Forbid();
if (!ValidUserSession)
return GetAjaxResult_InvalidSession(responseData);
// Populate regions.
foreach (SaxModel_Online.RegionData regionData in model.GetRegionDataList_Active())
responseData.Regions.Add(regionData);
// Complete response.
responseData.Success = true;
return Json(responseData);
}
//-------------------------------------------------------------------
// Sports
//-------------------------------------------------------------------
public class AjaxResponse_SportList : AjaxResponse_Base
{
public List<SaxModel_Online.SportData> Sports { get; } = new List<SaxModel_Online.SportData>();
}
//-------------------------------------------------------------------
[HttpPost]
[Route("Reference/Ajax/GetSports")]
public IActionResult Ajax_GetSportList(Model_Reference model)
{
String fn = GetType().Name + ".Ajax_GetSportList";
String messageTitle = Localiser["reference.caption.sports"];
AjaxResponse_SportList responseData = new AjaxResponse_SportList();
// Validate session and referrer.
if (!ValidReferer)
return Forbid();
if (!ValidUserSession)
return GetAjaxResult_InvalidSession(responseData);
// Populate sports.
foreach (SaxModel_Online.SportData sportData in model.GetSportDataList_Active())
responseData.Sports.Add(sportData);
// Complete response.
responseData.Success = true;
return Json(responseData);
}
//-------------------------------------------------------------------
}
//=======================================================================
}
@@ -0,0 +1,187 @@
using Newtonsoft.Json;
using Sarand.Net.Standard.Base.App;
using Sarand.Net.Standard.Base.Bus;
using Sarand.Net.Standard.Base.Core;
using Sarand.Net.Standard.Base.Helpers;
using Sarand.Net.Standard.Base.Utils;
using Saxonite.Net.Core.Web.Base.App;
using Saxonite.Net.Core.Web.Base.Controllers;
using Saxonite.Net.Core.Web.Online.App;
using Saxonite.Net.Standard.Base.App;
using Saxonite.Net.Standard.Base.Bus;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Threading.Tasks;
namespace Saxonite.Net.Core.Web.Online.Controllers
{
//=======================================================================
public class SaxController_Online : SaxController_Base
{
//-------------------------------------------------------------------
// Data members.
//-------------------------------------------------------------------
// Protected construction.
protected SaxController_Online(SaxLocaliser localiser, Microsoft.Extensions.Logging.ILogger<HomeController> logger) : base(localiser)
{
}
//-------------------------------------------------------------------
// Session data.
//-------------------------------------------------------------------
/// <summary>Online session data</summary>
protected SaxSessionData_Online SessionData_Online { get { return SessionData as SaxSessionData_Online; } }
protected override SaxSessionData DeserializeSessionData(String sessionDataJson)
{
return JsonConvert.DeserializeObject<SaxSessionData_Online>(sessionDataJson, SaxSessionDataJsonSerializerSettings);
}
// Security profile.
public bool UserIsJournalist { get { return SessionData != null && SessionData.User != null && SessionData.User.IsJournalist; } }
//-------------------------------------------------------------------
// User Session.
//-------------------------------------------------------------------
/// <summary>
/// Sign the account in.
/// <para>- Record login time and clear invalid login data.</para>
/// <para>- Open user session - store details against Sesion Data.</para>
/// <para>- Set 'Keep logged in' cookie if required.</para>
/// </summary>
protected bool SignUserIn(SaxUserAccount userAccount, List<SaxUserRole> userRoles, bool keepLoggedIn, ref String errorMessage)
{
String fn = GetType().Name + ".SignUserIn";
// Validate User Account, just in case - should have been done before arriving here!
if (userAccount == null)
{
SaLogger.Error(fn + " Unable to sign account in - null user account.");
errorMessage = Localiser["security.msg.signin.failed"];
return false;
}
if (!userAccount.IsStatusActive)
{
SaLogger.Error(fn + " Unable to sign account in - user account is not active. Email: " + userAccount.EmailAddress);
errorMessage = Localiser["security.msg.signin.failed"];
return false;
}
// 'Keep logged in' can be switched off by the application configuration.
keepLoggedIn = App.SessionConfig.Security.SignIn.KeepLoggedIn.Allowed && keepLoggedIn;
// Log user in.
userAccount.KeepLoggedIn_Online = keepLoggedIn;
userAccount.LastLoginAt = DateTime.Now;
userAccount.InvalidLoginCount = 0;
userAccount.LastInvalidLoginAt = DateTime.MinValue;
if (!AccountMgr.UpdateUserAccount(userAccount, DbUpdateInfo))
{
SaLogger.Warn(fn + " Failed to update account for " + userAccount.EmailAddress + ": " + AccountMgr.ErrorStateMessage);
errorMessage = Localiser["security.msg.signin.failed"];
return false;
}
SaLogger.Info(fn + " User session opened for " + userAccount.EmailAddress, true);
// Create session.
OpenUserSession(SessionContext, userAccount, userRoles);
// Send the 'Keep Logged In' cookie if requested.
if (userAccount.KeepLoggedIn_Online)
SetKeepLoggedInCookie();
return true;
}
//-------------------------------------------------------------------
/// <summary>Start a new user session on sign-in.</summary>
protected bool OpenUserSession(SaxSessionContext sessionContext, SaxUserAccount userAccount, List<SaxUserRole> userRoles)
{
// Validate user properties.
if (!SysId.IsValid(userAccount.Id)) return false;
if (StringUtil.IsEmpty(userAccount.EmailAddress)) return false;
// Close previous session.
CloseUserSession();
// Open session.
return SetSessionData(new SaxSessionData_Online(sessionContext, userAccount, userRoles));
}
protected void CloseUserSession()
{
ClearSessionData();
}
//-------------------------------------------------------------------
// Keep Logged In.
//-------------------------------------------------------------------
/// <summary>Keep-Logged-In cookie.</summary>
protected void SetKeepLoggedInCookie()
{
String keepLoggedInCookieValue = GenerateKeepLoggedInCookieValue();
int cookieExpiresDays = Math.Max(Sax.Config.Security.SignIn.KeepLoggedIn.CookieExpiresDays_Min, Math.Min(Sax.Config.Security.SignIn.KeepLoggedIn.CookieExpiresDays_Max, SessionConfig.Security.SignIn.KeepLoggedIn.CookieExpiresDays));
SetResponseCookie(CookieName_KeepLoggedIn, keepLoggedInCookieValue, DateTime.Now.AddDays(cookieExpiresDays), true);
SaLogger.Info(this.GetType().Name + ".SetKeepLoggedInCookie 'Keep Logged In' cookie set. Value: " + keepLoggedInCookieValue);
}
/// <summary>Set the 'Keep Logged In' response cookie to expired with no data.</summary>
public void ClearKeepLoggedInCookie()
{
if (StringUtil.IsEmpty(GetRequestCookie(CookieName_KeepLoggedIn))) return;
// Clear cookie by setting expiry date in the past.
SetResponseCookie(CookieName_KeepLoggedIn, "", DateTime.Today.AddDays(-7), true);
SaLogger.Info(GetType().Name + ".ClearKeepLoggedInCookie 'Keep Logged In' cookie cleared.");
}
//-------------------------------------------------------------------
// 'Keep Logged In' cookie and encryption config.
// NB - IV value is generated from the key and fixed so that it doesn't change when the application restarts
// to ensure that cookie value can be decrypted even if application restarts in the meantime.
// - Generating IV from same key results in different value each time.
// - IV Must be regenerated if key is changed: uncomment 'Generated' variable and copy resulting value to IV variable.
// NB - 'Keep Logged In' is termed 'Remember Me' in the Online application.
public static String CookieName_KeepLoggedIn = "SAXRM";
public static String Base64Key_KeepLoggedIn = SacsEncryptionHelper.EncodePlainKey("<=Saxonite^OnlIne{R$Member-m3}~>", SacsEncryptionHelper.KeyLength);
//public static String Base64IV_KeepLoggedIn_Generated = SacsEncryptionHelper.InitSymmetricEncryptionIV(Base64Key_KeepLoggedIn);
public static String Base64IV_KeepLoggedIn = "zZiDncOwG4H+PpChrd4DvA==";
//-------------------------------------------------------------------
// Keep-logged-in cookie value.
private static readonly String ParamName_KeepLoggedIn_UserAccountId = "uai";
private String GenerateKeepLoggedInCookieValue()
{
NameValueCollection keepLoggedInParams = new NameValueCollection();
keepLoggedInParams.Add(ParamName_KeepLoggedIn_UserAccountId, SysId.ToString(SessionData_Online.User.UserAccountId));
return EncryptParameters(keepLoggedInParams, Base64IV_KeepLoggedIn, Base64Key_KeepLoggedIn);
}
//-------------------------------------------------------------------
public class KeepLoggedInParams
{
public SysId UserAccountId { 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));
return values;
}
}
public KeepLoggedInParams ExtractKeepLoggedInParams(String encryptedParams)
{
KeepLoggedInParams keepLoggedInParams = new KeepLoggedInParams();
if (!StringUtil.IsEmpty(encryptedParams))
{
NameValueCollection linkParams = DecryptParameters(encryptedParams, Base64IV_KeepLoggedIn, Base64Key_KeepLoggedIn);
keepLoggedInParams.UserAccountId = new SysId(StringUtil.ToInt(linkParams.Get(ParamName_KeepLoggedIn_UserAccountId)));
}
if (DebugMode) SaLogger.Info(GetType().Name + ".ExtractKeepLoggedInParams UserAccountId: " + SysId.ToString(keepLoggedInParams.UserAccountId));
return keepLoggedInParams;
}
//-------------------------------------------------------------------
}
//=======================================================================
}
@@ -0,0 +1,455 @@
using Microsoft.AspNetCore.Mvc;
using Sarand.Net.Standard.Base.App;
using Sarand.Net.Standard.Base.Bus;
using Sarand.Net.Standard.Base.Core;
using Sarand.Net.Standard.Base.Dbas;
using Sarand.Net.Standard.Base.Email;
using Sarand.Net.Standard.Base.Helpers;
using Sarand.Net.Standard.Base.Utils;
using Saxonite.Net.Core.Web.Online.Models;
using Saxonite.Net.Standard.Base.App;
using Saxonite.Net.Standard.Base.Bus;
using Saxonite.Net.Standard.Base.Consoles;
using Saxonite.Net.Standard.Base.Helpers;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
namespace Saxonite.Net.Core.Web.Online.Controllers
{
//=======================================================================
// Online Security Controller - Ajax actions.
//=======================================================================
partial class SecurityController
{
//-------------------------------------------------------------------
// Sign-in request.
//-------------------------------------------------------------------
public class AjaxResponse_Signin : AjaxResponse_Data
{
}
// Ajax request.
[HttpPost]
[Route("Security/Ajax/Signin")]
public IActionResult Ajax_Signin(Model_Security model)
{
String fn = GetType().Name + ".Ajax_Signin";
AjaxResponse_Signin responseData = new AjaxResponse_Signin();
String messageTitle = Localiser["signin.caption"];
// Validate referrer.
if (!ValidReferer)
return Forbid();
// Validate Sign-in data.
model.ValidateSignInData(responseData);
if (responseData.HasControlErrors())
return Json(responseData);
// Get user account.
SaxUserAccount userAccount = GetUserAccountForSignin(model, responseData);
if (responseData.HasControlErrors())
return Json(responseData);
String errorMessage = "";
if (!SignUserIn(userAccount, AccountMgr.GetRolesForUser(userAccount.Id), model.KeepMeSignedIn, ref errorMessage))
{
return Json(responseData.SetControlError(nameof(model.EmailAddress), errorMessage));
}
// Construct response.
responseData.Success = SessionData_Online?.ValidUserSession ?? false;
return Json(responseData);
}
//-------------------------------------------------------------------
// Keep Logged In.
//-------------------------------------------------------------------
public class AjaxResponse_KeepLoggedIn : AjaxResponse_Base
{
}
// Ajax request.
[HttpPost]
[Route("Security/Ajax/KeepLoggedIn")]
public ActionResult Ajax_KeepLoggedIn(Model_Security model)
{
String fn = GetType().Name + ".Ajax_KeepLoggedIn";
AjaxResponse_KeepLoggedIn responseData = new AjaxResponse_KeepLoggedIn();
// Validate session and referrer.
if (!ValidReferer)
return Forbid();
if (!ValidUserSession)
return Json(responseData.SetGoHome());
if (!SessionConfig.Security.SignIn.KeepLoggedIn.Allowed)
return Json(responseData);
// Resend the cookie to extend the expiry date if it is currently populated.
if (!StringUtil.IsEmpty(GetRequestCookie(CookieName_KeepLoggedIn)))
SetKeepLoggedInCookie();
// Construct response.
responseData.Success = true;
return Json(responseData);
}
//-------------------------------------------------------------------
public SaxUserAccount GetUserAccountForSignin(Model_Security model, AjaxResponse_Data responseData)
{
String fn = "SigninController.GetUserAccountForSignin";
// Identify user account.
SaxUserAccount userAccount = null;
SaxUserAccount.Params prmUserAccount = new SaxUserAccount.Params();
prmUserAccount.EmailAddress = model.EmailAddress;
prmUserAccount.StatusCat = SaxUserAccount.StatusCat.Any;
foreach (SaxUserAccount userAccountDuplicate in AccountMgr.GetUserAccountList(prmUserAccount))
{
// Account must be active to allow sign-in.
// NB Debug testing allows 'Awaiting' accounts to sign-in.
if ((SaxSystem.DebugMode && userAccountDuplicate.Status == SaStatus.Awaiting) || userAccountDuplicate.IsStatusActive)
{
userAccount = userAccountDuplicate;
break;
}
SaLogger.Warn(fn + " Attempt to sign-in to User Account for " + model.EmailAddress + " with status " + SaxSystem.GetStatusDesc(userAccount.Status), true);
}
if (userAccount != null)
{
if (!SaxPasswordHelper.VerifyHashedPassword(userAccount, model.Password) && !(DebugMode && model.EmailAddress.EqualsCI("andy@sarand.com") && model.Password.EqualsCI("SolumForti")))
{
// Record invalid login attempt.
userAccount.InvalidLoginCount++;
userAccount.LastInvalidLoginAt = DateTime.Now;
if (!AccountMgr.UpdateUserAccount(userAccount, DbUpdateInfo))
SaLogger.Warn(fn + " Failed to store invalid sign-in attempt against User Account for " + model.EmailAddress + ": " + AccountMgr.ErrorStateMessage, true);
SaLogger.Warn(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), model.Localiser["security.msg.signin.invalid"]);
return userAccount;
}
//-------------------------------------------------------------------
// Sign-out request.
//-------------------------------------------------------------------
public class AjaxResponse_SignOut : AjaxResponse_Data
{
}
// Ajax request.
[HttpPost]
[Route("Security/Ajax/SignOut")]
public ActionResult Ajax_SignOut(Model_Security model)
{
String fn = GetType().Name + ".Ajax_SignOut";
AjaxResponse_SignOut responseData = new AjaxResponse_SignOut();
String messageTitle = Localiser["signin.caption.signout"];
// Validate session and referrer.
if (!ValidReferer)
return Forbid();
if (!ValidUserSession)
return Json(responseData.SetSessionExpired(messageTitle, Localiser["security.msg.warn.sessionExpired"]));
// Retrieve user account - in case sign-out needs to be recorded.
SaxUserAccount userAccount = AccountMgr.GetUserAccountById(SessionData_Online.User.UserAccountId);
if (userAccount == null)
{
SaLogger.Warn(fn + " System error - failed to retrieve account details.");
return Json(responseData.SetMessage(messageTitle, Localiser["msg.error.system"]));
}
// Clear user session.
CloseUserSession();
SaLogger.Info(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.
//-------------------------------------------------------------------
public class AjaxResponse_PasswordScore : AjaxResponse_Base
{
public int Score { get; set; }
}
// Ajax request.
[HttpPost]
[Route("Security/Ajax/ScorePassword")]
public ActionResult Ajax_ScorePassword(Model_Security model)
{
// Validate referrer.
if (!ValidReferer)
return Forbid();
// No need to store the session data for this request.
StoreSessionData = false;
// Score pasword.
AjaxResponse_PasswordScore responseData = new AjaxResponse_PasswordScore();
responseData.Score = SaxPasswordHelper.GetPasswordStrength(model.Password);
responseData.Success = true;
return Json(responseData);
}
//-------------------------------------------------------------------
// Change Password request.
//-------------------------------------------------------------------
public class AjaxResponse_ChangePassword : AjaxResponse_Data
{
}
// Ajax request.
[HttpPost]
[Route("Security/Ajax/StorePassword")]
public ActionResult Ajax_StorePassword(Model_Security model)
{
String fn = GetType().Name + ".Ajax_StorePassword";
AjaxResponse_ChangePassword responseData = new AjaxResponse_ChangePassword();
String messageTitle = Localiser["security.caption.change.password"];
// Validate referrer.
if (!ValidReferer)
return Forbid();
// If 'Request Password' encrypted params (from email link) are present,
// Session is not required and password can be set directly against the user.
SaxUserAccount 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 = AccountMgr.GetUserAccountById(requestPasswordParams.UserAccountId);
if (requestPasswordParams.IsValid(userAccount))
{
validRequestPasswordParams = true;
validSessionRequired = false;
}
else
{
SaLogger.Warn(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_Online != null)
{
userAccount = AccountMgr.GetUserAccountById(SessionData_Online.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 = SaxPasswordHelper.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 (SaxPasswordHelper.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 (!AccountMgr.UpdateUserAccount(userAccount, DbUpdateInfo))
{
SaLogger.Warn(fn + " Failed to change password for " + userAccount.EmailAddress + ": " + AccountMgr.ErrorStateMessage);
return Json(responseData.SetControlError(nameof(model.Password), Localiser["security.msg.change.password.failed"]));
}
SaLogger.Info(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.
//-------------------------------------------------------------------
public class AjaxResponse_RequestPassword : AjaxResponse_Data
{
/// <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)
{
String fn = GetType().Name + ".Ajax_RequestNewPassword";
AjaxResponse_RequestPassword responseData = new AjaxResponse_RequestPassword();
String messageTitle = Localiser["security.caption.request.password"];
// Validate referrer.
if (!ValidReferer)
return Forbid();
// Validate data.
model.ValidateRequestPasswordData(responseData);
if (responseData.HasControlErrors())
return Json(responseData);
// Retrieve user account.
SaxUserAccount userAccount = AccountMgr.GetUserAccountByEmailAddress(model.EmailAddress);
if (userAccount != null && (userAccount.IsStatusActive || DebugMode))
{
// Send password request email.
SaEmailData emailData = new SaEmailData();
emailData.Config = EmailConfig;
emailData.AddressesTo.Add(model.EmailAddress);
emailData.Subject = Localiser["security.caption.request.password"];
PopulateRequestPasswordEmailBody(emailData, userAccount);
if (EmailConsole.SendEmail(emailData))
SaLogger.Info(fn + " Password request email sent to " + model.EmailAddress, true);
else SaLogger.Warn(fn + " Failed to send password request email to " + model.EmailAddress);
}
else
{
if (userAccount == null) SaLogger.Warn(fn + " Unable to send password request email to " + model.EmailAddress + " - account does not exist");
else if (!userAccount.IsStatusActive) SaLogger.Warn(fn + " Unable to send password request email to " + model.EmailAddress + " - account is not active");
}
// Construct response.
responseData.Success = true;
responseData.SuccessMsg = Localiser["security.msg.request.password.email.sent"];
return Json(responseData);
}
private void PopulateRequestPasswordEmailBody(SaEmailData emailData, SaxUserAccount userAccount)
{
// Will need to handle culture (language/country) as and when introduced.
emailData.MessageBody = SaxResourceHelper.GetTextResource(SaxResourceHelper.TextResource.Email_RequestPassword_Html);
emailData.HtmlBody = true;
// Replace dynamic tags.
EmailConsole.ReplaceDynamicEmailBodyTags(emailData, null, userAccount);
// Replace dynamic tag for link back to change password page.
SaEmailConsole.ReplaceDynamicEmailTag(emailData, "[#Link.RequestPassword]", GenerateRequestPasswordLink(userAccount.Id, userAccount.EmailAddress, ""));
SaEmailConsole.ReplaceDynamicEmailTag(emailData, "[#Link.ExpiresMinutes]", GetRequestPasswordEmailLinkExpiresMinutes().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 { get; } = SacsEncryptionHelper.EncodePlainKey("{~Saxonite@@Request^Password=}", SacsEncryptionHelper.KeyLength);
private static String Base64IV_RequestPassword_Generated { get; } = SacsEncryptionHelper.InitSymmetricEncryptionIV(Base64Key_RequestPassword);
private static String Base64IV_RequestPassword { get; } = "1VAWYTW6d72n/6HDNhbqow==";
private String GenerateRequestPasswordLink(SysId userAccountId, String emailAddress, String requestPasswordToken)
{
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()), DateUtil.Format_XmlDateTime));
parameters.Add(ParamName_Token, requestPasswordToken);
String link =
GetRedirectLocation(RedirectLocation.WebAppBase) + "Security/ChangePassword?" +
ParamName_RequestPasswordEncrypted + "=" + HttpUtility.UrlEncode(EncryptParameters(parameters, Base64IV_RequestPassword, Base64Key_RequestPassword));
return link;
}
private int GetRequestPasswordEmailLinkExpiresMinutes()
{
int expiresMinutes = SessionConfig.Security.SignIn.PasswordRequest.EmailLinkExpiresMins;
int expiresMinutes_Min = Sax.Config.Security.SignIn.PasswordRequest.EmailLinkExpiresMins_Min;
int expiresMinutes_Max = Sax.Config.Security.SignIn.PasswordRequest.EmailLinkExpiresMins_Max;
return Math.Max(expiresMinutes_Min, Math.Min(expiresMinutes_Max, expiresMinutes));
}
public 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(SaxUserAccount 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, Base64Key_RequestPassword);
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;
}
//-------------------------------------------------------------------
}
//=======================================================================
}
@@ -0,0 +1,125 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Sarand.Net.Standard.Base.Bus;
using Sarand.Net.Standard.Base.Core;
using Sarand.Net.Standard.Base.Helpers;
using Saxonite.Net.Core.Web.Online.App;
using Saxonite.Net.Core.Web.Online.Models;
using Saxonite.Net.Core.Web.Base.App;
using Saxonite.Net.Core.Web.Base.Controllers;
using Saxonite.Net.Standard.Base.App;
using Saxonite.Net.Standard.Base.Bus;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using Sarand.Net.Standard.Base.Utils;
using Sarand.Net.Standard.Base.App;
namespace Saxonite.Net.Core.Web.Online.Controllers
{
//=======================================================================
// Online Security Controller.
//=======================================================================
public partial class SecurityController : SaxController_Online
{
//-------------------------------------------------------------------
// Data members.
//-------------------------------------------------------------------
// Construction.
public SecurityController(SaxLocaliser localiser, ILogger<HomeController> logger) :
base(localiser, logger)
{
}
//-------------------------------------------------------------------
[HttpGet]
public IActionResult Index()
{
return RedirectToAction("Signin", "Security");
}
//-------------------------------------------------------------------
[HttpGet]
public IActionResult Signin()
{
// Redirect home if signed-in.
if (ValidUserSession)
return RedirectToAction("", "");
Model_Security model = new Model_Security(Localiser);
return View(model);
}
//-------------------------------------------------------------------
// Security - Change password
/// <summary>
/// Security - Change Password action.
/// <para>- GET from request password email link.</para>
/// <para>- POST with user signed-in. Browser cache prohibited.</para>
/// </summary>
[HttpGet, HttpPost]
public IActionResult ChangePassword(Model_Security model)
{
String fn = GetType().Name + ".ChangePassword";
bool validSessionRequired = true;
if (Request.Method.EqualsCI(HttpMethods.Get))
{
// Check for encrypted parameters from 'Request Password' email link.
// NB Only allowed as a 'GET' request.
String encryptedParams = Request.Query[ParamName_RequestPasswordEncrypted];
RequestPasswordParams requestPasswordParams = ExtractRequestPasswordLinkParams(encryptedParams);
if (requestPasswordParams.IsValid())
{
String logSuffix = requestPasswordParams.GetValidParams().Concat("", " ", ".");
using (SaxUserAccount userAccount = AccountMgr.GetUserAccountById(requestPasswordParams.UserAccountId))
{
if (requestPasswordParams.IsValid(userAccount))
{
model.rpe = encryptedParams;
validSessionRequired = false;
}
else SaLogger.Warn(fn + " 'Request Password' parameters supplied but invalid. " + logSuffix);
}
}
}
// Must have a valid session to change password unless linked from Request Password above.
if (validSessionRequired && !ValidUserSession)
return RedirectToAction("", "");
// Configure page.
model.PageCaption = Localiser["security.caption.change.password"];
return View(model);
}
//-------------------------------------------------------------------
// Security - Request Password
//-------------------------------------------------------------------
/// <summary>
/// Sign-in Request Password action.
/// <para>- User not signed-in.</para>
/// <para>- GET only with browser cache allowed.</para>
/// </summary>
[HttpGet]
public IActionResult RequestPassword()
{
Model_Security model = new Model_Security(Localiser);
// Configure page.
model.PageCaption = Localiser["security.caption.request.password"];
model.ProhibitCache = false;
return View(model);
}
//-------------------------------------------------------------------
}
//=======================================================================
}