Files
BusinessMgr-2019/SaBusMgrCoreWeb/Web/Controllers/SbmController_Base.cs
T

524 lines
21 KiB
C#

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Routing;
using Microsoft.Net.Http.Headers;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Sarand.BusinessMgr.Net.Core.Web.App;
using Sarand.BusinessMgr.Net.Core.Web.Models;
using Sarand.BusinessMgr.Net.Standard.Base.App;
using Sarand.BusinessMgr.Net.Standard.Base.Bus;
using Sarand.BusinessMgr.Net.Standard.Base.Consoles;
using Sarand.BusinessMgr.Net.Standard.Base.Mgr;
using Sarand.Net.Standard.Base.App;
using Sarand.Net.Standard.Base.CodeTypes;
using Sarand.Net.Standard.Base.Core;
using Sarand.Net.Standard.Base.Dbas;
using Sarand.Net.Standard.Base.Diary;
using Sarand.Net.Standard.Base.Email;
using Sarand.Net.Standard.Base.Helpers;
using Sarand.Net.Standard.Base.Interfaces;
using Sarand.Net.Standard.Base.ItemData;
using Sarand.Net.Standard.Base.ItemDocs;
using Sarand.Net.Standard.Base.ItemOptions;
using Sarand.Net.Standard.Base.ReferenceData;
using Sarand.Net.Standard.Base.Security;
using Sarand.Net.Standard.Base.UpdateLog;
using Sarand.Net.Standard.Base.Utils;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
namespace Sarand.BusinessMgr.Net.Core.Web.Controllers
{
//=======================================================================
public abstract class SbmController_Base : Controller
{
//-------------------------------------------------------------------
// Data members.
private SbmInternals internals;
//-------------------------------------------------------------------
// Protected construction.
protected SbmController_Base(SbmLocaliser localiser)
{
Localiser = localiser;
// Initialise console (debug) logging listener.
//if (Directory.Exists(App.LoggingRootFolder) && SbmSystem.DebugMode)
// SbmConsoleListener.ConfigureDebugListener(App.LoggingRootFolder);
if (DebugMode) SaLogger.Trace(GetType().Name + ".CTR");
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
}
base.Dispose(disposing);
}
//-------------------------------------------------------------------
// Properties.
public bool DebugMode { get; } = SacsSystem.DebugMode;
protected SbmApplication_Web App { get { return SbmGlobals_Web.Application; } }
public SbmAppConfig AppConfig { get { return App.AppConfig; } }
public SbmSessionContext SessionContext { get { return UserContext?.SessionContext ?? null; } }
public SbmUserContext UserContext { get { return SessionData.UserContext; } }
protected SbmLocaliser Localiser { get; }
/// <summary>
/// Indicate whether session data should be stored by action.
/// <para>Any action that might modify the session data must store it.</para>
/// <para>Some actions such as scoring a password's complexity don't need to.</para>
/// </summary>
public virtual bool StoreSessionData { get; protected set; } = true;
// Culture.
private string LanguageIso2
{
get
{
var feature = HttpContext.Features.Get<IRequestCultureFeature>();
return feature.RequestCulture.UICulture.TwoLetterISOLanguageName.ToLower();
}
}
// Business components.
public SbmInternals Internals { get { if (internals == null) internals = new SbmInternals(UserContext); return internals; } }
// Managers.
public SbmAccountMgr AccountMgr { get { return Internals.AccountMgr; } }
public SbmAssetMgr AssetMgr { get { return Internals.AssetMgr; } }
public SbmCommodityMgr CommodityMgr { get { return Internals.CommodityMgr; } }
public SbmPriceMgr PriceMgr { get { return Internals.PriceMgr; } }
public SbmTransactionMgr TransactionMgr { get { return Internals.TransactionMgr; } }
public SbmStockMgr StockMgr { get { return Internals.StockMgr; } }
public SbmProcessMgr ProcessMgr { get { return Internals.ProcessMgr; } }
public SbmDataItemMgr DataItemMgr { get { return Internals.DataItemMgr; } }
// Consoles.
public SbmConfigConsole ConfigConsole { get { return Internals.ConfigConsole; } }
public SaDiaryConsole DiaryConsole { get { return Internals.DiaryConsole; } }
public SaSecurityConsole SecurityConsole { get { return Internals.SecurityConsole; } }
public SaCodeTypeConsole CodeTypeConsole { get { return Internals.CodeTypeConsole; } }
public SaReferenceDataConsole ReferenceDataConsole { get { return Internals.ReferenceDataConsole; } }
public SaUpdateLogConsole UpdateLogConsole { get { return Internals.UpdateLogConsole; } }
public SaInterfaceConsole InterfaceConsole { get { return Internals.InterfaceConsole; } }
public SaItemDocConsole ItemDocConsole { get { return Internals.ItemDocConsole; } }
public SaItemOptionConsole ItemOptionConsole { get { return Internals.ItemOptionConsole; } }
public SaItemDataConsole ItemDataConsole { get { return Internals.ItemDataConsole; } }
// Configurations.
public SbmStockControlConfig StockControlConfig { get { return Internals.StockControlConfig; } }
public SbmRequirementCostingConfig RequirementCostingConfig { get { return Internals.RequirementCostingConfig; } }
// System codes.
public List<TypeCodeDesc> GetCodesOfType(Sbm.SystemCode systemCode) { return Internals.GetCodesOfType(systemCode); }
// Email.
public SaEmailConfig EmailConfig { get { return Internals.EmailConfig; } }
public SbmEmailConsole EmailConsole { get { return Internals.EmailConsole; } }
//-------------------------------------------------------------------
// Redirection locations.
//-------------------------------------------------------------------
public static readonly String ParamName_SessionExpired = "SessionExpired";
protected enum RedirectLocation
{
WebAppBase,
WebAppHome,
WebAppHome_SessionExpired,
}
protected String GetRedirectLocation(RedirectLocation redirectLocation)
{
string protocol = Request.Scheme;
var parameters = new RouteValueDictionary();
String homeAction = Url.Action("Index", "Home", parameters, protocol);
switch (redirectLocation)
{
case RedirectLocation.WebAppBase:
return Regex.Replace(homeAction, "\\/$", "") + "/";
case RedirectLocation.WebAppHome:
return homeAction;
case RedirectLocation.WebAppHome_SessionExpired:
// Set the session expired parameter so that the page can report it.
// NB Paramater must have a value to be included in the URL query string.
parameters.Add(ParamName_SessionExpired, "1");
return homeAction;
}
return "";
}
//-------------------------------------------------------------------
// Action execution events.
//-------------------------------------------------------------------
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (DebugMode) SaLogger.Debug(GetType().Name + ".OnActionExecuting" + (ViewData.Model != null ? " Model: " + ViewData.Model : ""));
// Refreh model properties.
SbmModel_Base baseModel = ViewData.Model as SbmModel_Base;
RefreshModel(baseModel);
base.OnActionExecuting(filterContext);
}
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
if (DebugMode) SaLogger.Debug(GetType().Name + ".OnActionExecuted" + (ViewData.Model != null ? " Model: " + ViewData.Model : ""));
// Refreh model properties.
SbmModel_Base baseModel = ViewData.Model as SbmModel_Base;
RefreshModel(baseModel);
// Store session data if present - as potentially modfied.
if (SessionData != null)
{
// Most actions will store the session data in case it's been modified.
// Some actions (eg. score password) don't need to.
if (StoreSessionData)
{
String sessionDataJson = JsonConvert.SerializeObject(SessionData);
HttpContext.Session.Set(SessionStorage.SessionKey_SessionData, SessionStorage.sessionDataEncoding.GetBytes(sessionDataJson));
}
}
else HttpContext.Session.Remove(SessionStorage.SessionKey_SessionData);
// Response headers.
if (baseModel != null && baseModel.ProhibitCache)
{
if (Response.Headers.ContainsKey(HeaderNames.CacheControl))
Response.Headers.Remove(HeaderNames.CacheControl);
Response.Headers.Add(HeaderNames.CacheControl, "no-cache, no-store, must-revalidate");
Response.Headers.Add(HeaderNames.Pragma, "no-cache");
Response.Headers.Add(HeaderNames.Expires, "0");
}
base.OnActionExecuted(filterContext);
}
//-------------------------------------------------------------------
// Model.
//-------------------------------------------------------------------
protected void RefreshModel(SbmModel_Base model)
{
if (model == null) return;
// Session data.
model.SessionData_Web = SessionData;
// Redirect locations.
model.RedirectLocation.Base = GetRedirectLocation(RedirectLocation.WebAppBase);
model.RedirectLocation.Home = GetRedirectLocation(RedirectLocation.WebAppHome);
model.RedirectLocation.Home_SessionExpired = GetRedirectLocation(RedirectLocation.WebAppHome_SessionExpired);
}
//-------------------------------------------------------------------
// Parameters.
//-------------------------------------------------------------------
public String EncryptParameters(NameValueCollection parameters, String base64IV, String base64Key)
{
String fn = nameof(ControllerBase) + ".EncryptParameters";
try
{
// Copy the list of parameters, adding a random name/value pair to randomise the encryption.
NameValueCollection parametersToEncrypt = new NameValueCollection(parameters);
// NB Range 10-189 intended to have equal chance of 2 or 3 digit number.
parametersToEncrypt.Add("!" + SacsRandomHelper.GetInt32(10, 190).ToString(), SacsRandomHelper.GetInt32(10, 190).ToString());
// Sort the keys to ensure that the random '!' param is first in the query string.
String[] paramKeys = parametersToEncrypt.AllKeys;
Array.Sort(paramKeys);
// Serialise parameters as if in a URL query string - so that they can be deserialised in DecryptParameters() below.
string queryString = String.Join("&", paramKeys.Select(a => a + "=" + HttpUtility.UrlEncode(parametersToEncrypt[a])));
String encrypted = SacsEncryptionHelper.Encrypt(queryString, base64IV, base64Key);
if (DebugMode)
{
SaLogger.Debug(fn + " Encrypted: " + encrypted);
NameValueCollection decryptedParams = DecryptParameters(encrypted, base64IV, base64Key);
}
return encrypted;
}
catch (Exception exc) { SaLogger.Warn(fn + " " + ExceptionUtil.GetErrorStateMessage(exc)); }
return "";
}
public NameValueCollection DecryptParameters(String encrypted, String base64IV, String base64Key)
{
String fn = nameof(ControllerBase) + ".DecryptParameters";
if (DebugMode) SaLogger.Debug(fn + " \n\tEncrypted: " + encrypted + "\n\tIV: " + base64IV + "\n\tKey: " + base64Key);
try
{
String queryString = SacsEncryptionHelper.Decrypt(encrypted, base64IV, base64Key);
return HttpUtility.ParseQueryString(queryString);
}
catch (Exception exc) { SaLogger.Warn(fn + " " + ExceptionUtil.GetErrorStateMessage(exc)); }
return new NameValueCollection();
}
//-------------------------------------------------------------------
// Cookies.
//-------------------------------------------------------------------
/// <summary>Retrieve the value from the named request cookie.</summary>
public String GetRequestCookie(String key)
{
if (Request.Cookies.ContainsKey(key))
return Request.Cookies[key];
return null;
}
/// <summary>Set the value against the named response cookie.</summary>
public void SetResponseCookie(string key, string value, DateTime? expiresAt = null, bool isEssential = false)
{
CookieOptions options = new CookieOptions();
options.HttpOnly = true;
options.Secure = true;
options.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict;
if (expiresAt.HasValue)
options.Expires = expiresAt;
if (isEssential)
options.IsEssential = true;
Response.Cookies.Append(key, value, options);
}
//-------------------------------------------------------------------
// Session.
//-------------------------------------------------------------------
public bool ValidUserSession { get { return SessionData?.ValidUserSession ?? false; } }
public DbUpdateInfo DbUpdateInfo { get { return SessionData?.DbUpdateInfo ?? new DbUpdateInfo(null, "", SbmSystem.ComputerName); } }
// Referrer.
public String Referer { get { return Request.Headers["Referer"].ToString(); } }
public bool ValidReferer { get { return !StringUtil.IsEmpty(Referer); } }
/// <summary>Store session data, eg. to start a new user session on sign-in.</summary>
protected bool SetSessionData(SbmSessionData_Web sessionData)
{
if (sessionStorage.SessionData != null) return false;
sessionStorage.SessionData = sessionData;
return true;
}
protected void ClearSessionData()
{
if (HttpContext.Session.IsAvailable)
HttpContext.Session.Clear();
sessionStorage.SessionData = null;
}
//-------------------------------------------------------------------
// Session storage.
public class SessionStorage
{
public SbmSessionData_Web SessionData { get; set; }
public static String SessionKey_SessionData { get; } = "SbmSessionData";
public static Encoding sessionDataEncoding { get; } = Encoding.UTF8;
//JSON Serialisation.
public class SbmSessionDataContractResolver : DefaultContractResolver
{
// To allow private properties to be deserialised.
// NB Only applies to top level properties in the serialised object.
protected override List<MemberInfo> GetSerializableMembers(Type objectType)
{
var flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
MemberInfo[] fields = objectType.GetFields(flags);
return fields
.Concat(objectType.GetProperties(flags).Where(propInfo => propInfo.CanWrite))
.ToList();
}
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
return base.CreateProperties(type, MemberSerialization.Fields);
}
};
public static JsonSerializerSettings JsonDeserialisationSettings = new JsonSerializerSettings
{
ContractResolver = new SbmSessionDataContractResolver(),
// Force all deserialised objects to be replaced - to avoid proxy lists not populating.
ObjectCreationHandling = ObjectCreationHandling.Replace
};
}
private readonly SessionStorage sessionStorage = new SessionStorage();
protected JsonSerializerSettings SbmSessionDataJsonSerializerSettings { get { return SessionStorage.JsonDeserialisationSettings; } }
//-------------------------------------------------------------------
public SbmSessionData_Web SessionData
{
get
{
try
{
// Retrieve session data if not already in controller.
if (sessionStorage.SessionData == null)
{
byte[] sessionDataBytes = null;
HttpContext.Session.TryGetValue(SessionStorage.SessionKey_SessionData, out sessionDataBytes);
if (sessionDataBytes != null)
{
String sessionDataJson = SessionStorage.sessionDataEncoding.GetString(sessionDataBytes);
sessionStorage.SessionData = DeserializeSessionData(sessionDataJson);
}
}
return sessionStorage.SessionData;
}
catch (Exception exc)
{
SaLogger.Warn(GetType().Name + ".SessionData " + exc.GetType().Name + " - " + exc.Message + exc.GetStackEntries().Concat("", "\n\t"));
}
return null;
}
}
protected abstract SbmSessionData_Web DeserializeSessionData(String sessionDataJson);
//-------------------------------------------------------------------
// Ajax actions.
//-------------------------------------------------------------------
protected static readonly Encoding ajaxResponseEncoding = Encoding.UTF8;
protected IActionResult GetAjaxResult_GoHome(AjaxResponse_Base responseData)
{
responseData.SetGoHome();
return Json(responseData);
}
protected IActionResult GetAjaxResult_InvalidSession(AjaxResponse_Base responseData)
{
responseData.SetGoHome();
return Json(responseData);
}
//-------------------------------------------------------------------
public class AjaxResponse_Base
{
// Response status.
public bool Success { get; set; } = false;
public bool Error { get; set; } = false;
public bool SessionExpired { get; protected set; } = false;
public bool GoHome { get; protected set; } = false;
public AjaxResponse_Base SetSessionExpired(String title, String text)
{
SessionExpired = true;
return SetMessage(title, text);
}
public AjaxResponse_Base SetGoHome()
{
GoHome = true;
return this;
}
public AjaxResponse_Base SetGoHome(String title, String text)
{
GoHome = true;
return SetMessage(title, text);
}
// Message - displays alert popup.
public class MessageData
{
public String Title { get; set; }
public String Text { get; set; }
}
public MessageData Message { get; private set; }
public AjaxResponse_Base SetMessage(String title, String text)
{
if (!StringUtil.IsEmpty(text))
Message = new MessageData() { Title = title, Text = text };
return this;
}
}
//-------------------------------------------------------------------
public class AjaxResponse_Data : AjaxResponse_Base
{
// Construction.
protected AjaxResponse_Data(SbmModel_Base model)
{
}
// Form data.
public class FormControlValue
{
public FormControlValue(String controlName, String value, String error)
{
ControlName = controlName;
Value = value;
Error = error;
}
public String ControlName { get; }
public String Value { get; set; }
public String Error { get; set; }
}
private readonly Dictionary<String, FormControlValue> controlValues = new Dictionary<String, FormControlValue>();
public List<FormControlValue> ControlValues { get { return controlValues.Values.ToList(); } }
public void SetControlValue(String controlName, String value, String error = null)
{
if (controlValues.ContainsKey(controlName))
{
// Modify existing entry in the list.
FormControlValue controlValue = controlValues[controlName];
if (value != null) controlValue.Value = value;
if (error != null) controlValue.Error = error;
}
else
{
// Add new control to list.
controlValues.Add(controlName, new FormControlValue(controlName, value, error));
}
}
/// <summary>Assign the error to the control by name and return this response data.</summary>
public AjaxResponse_Data SetControlError(String controlName, String error)
{
return SetControlError(controlName, error, this);
}
/// <summary>
/// Assign the error to the control by name and return the specified value.
/// <para>- Allows alternative return values to be specified (eg. 'false') to avoid separate return statement in calling routine.</para>
/// </summary>
public T SetControlError<T>(String controlName, String error, T returnValue)
{
SetControlValue(controlName, null, error);
return returnValue;
}
public bool HasControlErrors()
{
foreach (FormControlValue controlValue in controlValues.Values)
{
if (!StringUtil.IsEmpty(controlValue.Error)) return true;
}
return false;
}
// Notification - briefly displays after response received.
public class NotificationData
{
public String Title { get; set; }
public String Message { get; set; }
}
public NotificationData Notification { get; private set; }
public bool HasNotification { get { return Notification != null; } }
public AjaxResponse_Data SetNotification(String title, String message)
{
Notification = new NotificationData() { Title = title, Message = message };
return this;
}
// Paged data.
public class PagingData
{
public PagingData(SbmModel_Base model) :
this(model.Paging.PageNo, model.Paging.PageLength, model.Paging.ItemCount, model.Paging.ItemTotal)
{ }
public PagingData(uint pageNo, ushort pageLength, ushort itemCount, ulong itemTotal)
{
PageNo = pageNo;
PageLength = pageLength;
ItemCount = itemCount;
ItemTotal = itemTotal;
}
public bool Paged { get { return (PageNo > 0 && PageLength > 0 && ItemTotal > PageLength); } }
public uint PageNo { get; } = 0;
public ushort PageLength { get; } = 0;
public uint PageCount { get { return (PageLength > 0 ? (uint)Math.Ceiling((decimal)ItemTotal / PageLength) : 0); } }
public ushort ItemCount { get; } = 0;
public ulong ItemTotal { get; } = 0;
}
public PagingData Paging { get; private set; }
public void SetPaging(SbmModel_Base model)
{
Paging = new PagingData(model);
}
}
//-------------------------------------------------------------------
}
//=======================================================================
}