546 lines
19 KiB
C#
546 lines
19 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 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 System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Specialized;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading.Tasks;
|
|
using System.Web;
|
|
|
|
namespace Sacs.Web.Components
|
|
{
|
|
//=======================================================================
|
|
public class SaController : Controller
|
|
{
|
|
//-------------------------------------------------------------------
|
|
/// <summary>
|
|
/// Protected construction - use derived objects.
|
|
/// </summary>
|
|
protected SaController()
|
|
{
|
|
}
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
}
|
|
base.Dispose(disposing);
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
// Properties.
|
|
public DateTime CreatedAt { get; } = DateTime.Now;
|
|
public bool DebugMode { get; } = SacsSystem.DebugMode;
|
|
/// <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 CultureInfo sessionCulture;
|
|
private CultureInfo RequestCulture { get { return Request.HttpContext.Features.Get<IRequestCultureFeature>()?.RequestCulture?.Culture ?? CultureInfo.InvariantCulture; } }
|
|
public CultureInfo SessionCulture
|
|
{
|
|
get
|
|
{
|
|
if (sessionCulture == null)
|
|
{
|
|
sessionCulture = RequestCulture;
|
|
// ToDo Adjust culture if needed.
|
|
}
|
|
return sessionCulture;
|
|
}
|
|
}
|
|
private string LanguageIso2
|
|
{
|
|
get
|
|
{
|
|
var feature = HttpContext.Features.Get<IRequestCultureFeature>();
|
|
return feature.RequestCulture.UICulture.TwoLetterISOLanguageName.ToLowerInvariant();
|
|
}
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
// Redirection locations.
|
|
//-------------------------------------------------------------------
|
|
public static readonly String ParamName_SessionExpired = "SessionExpired";
|
|
public enum RedirectLocation
|
|
{
|
|
WebAppBase,
|
|
WebAppHome,
|
|
WebAppHome_SessionExpired,
|
|
}
|
|
[NonAction]
|
|
public String GetRedirectLocation(RedirectLocation redirectLocation)
|
|
{
|
|
string protocol = Request.Scheme;
|
|
var parameters = new RouteValueDictionary();
|
|
String baseAction = Url.Action("Index", "", parameters, protocol);
|
|
String homeAction = Url.Action("Index", "Home", parameters, protocol);
|
|
switch (redirectLocation)
|
|
{
|
|
case RedirectLocation.WebAppBase:
|
|
return baseAction;
|
|
case RedirectLocation.WebAppHome:
|
|
return homeAction;
|
|
case RedirectLocation.WebAppHome_SessionExpired:
|
|
// Set the session expired parameter so that the page can report it.
|
|
// NB Parameter 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($"{SysUtil.Fn}" + (ViewData.Model != null ? " Model: " + ViewData.Model : ""));
|
|
|
|
base.OnActionExecuting(filterContext);
|
|
}
|
|
public override void OnActionExecuted(ActionExecutedContext filterContext)
|
|
{
|
|
//if (DebugMode) SaLogger.Debug($"{SysUtil.Fn}" + (ViewData.Model != null ? " Model: " + ViewData.Model : ""));
|
|
|
|
base.OnActionExecuted(filterContext);
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
public static String EncryptParameters(NameValueCollection parameters, String base64IV, String base64Key)
|
|
{
|
|
try
|
|
{
|
|
// Copy the list of parameters and sort the keys.
|
|
NameValueCollection parametersToEncrypt = new NameValueCollection(parameters);
|
|
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.
|
|
// Start with a random length parameter name at start of list to obscure the encryption.
|
|
string queryString = SacsRandomHelper.GetRandomLengthNumber(4) + "&";
|
|
queryString += String.Join("&", paramKeys.Select(a => a + "=" + HttpUtility.UrlEncode(parametersToEncrypt[a])));
|
|
|
|
// Encrypt data.
|
|
String encrypted = SacsEncryptionHelper.Encrypt(queryString, base64IV, base64Key);
|
|
//if (SacsSystem.DebugMode) SaLogger.Debug(SysUtil.Fn + "\n\tData: " + queryString + " \n\tEncrypted: " + encrypted);
|
|
return encrypted;
|
|
}
|
|
catch (Exception exc) { SaLogger.Warn($"{SysUtil.Fn} {ExceptionUtil.GetErrorStateMessage(exc)}"); }
|
|
return "";
|
|
}
|
|
public static NameValueCollection DecryptParameters(String encrypted, String base64IV, String base64Key)
|
|
{
|
|
try
|
|
{
|
|
String queryString = SacsEncryptionHelper.Decrypt(encrypted, base64IV, base64Key);
|
|
//if (SacsSystem.DebugMode) SaLogger.Debug(SysUtil.Fn + " \n\tEncrypted: " + encrypted + "\n\tData: " + queryString);
|
|
return HttpUtility.ParseQueryString(queryString);
|
|
}
|
|
catch (Exception exc) { SaLogger.Warn(SysUtil.Fn + " " + ExceptionUtil.GetErrorStateMessage(exc)); }
|
|
return new NameValueCollection();
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
// Cookies.
|
|
//-------------------------------------------------------------------
|
|
[NonAction]
|
|
/// <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;
|
|
}
|
|
[NonAction]
|
|
/// <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);
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
// Error State
|
|
//-------------------------------------------------------------------
|
|
private SacsErrorState errorState = null;
|
|
public bool HasErrorState()
|
|
{
|
|
return errorState != null;
|
|
}
|
|
public SacsErrorState ErrorState
|
|
{
|
|
get { return errorState; }
|
|
}
|
|
public String ErrorStateMessage
|
|
{
|
|
get { return (errorState != null ? errorState.Message : String.Empty); }
|
|
}
|
|
/// <summary>
|
|
/// Record the error state message.
|
|
/// <para>Blank message does not set the error state - leaves previous state intact.</para>
|
|
/// <para>Return value is fale unless the message is blank.</para>
|
|
/// </summary>
|
|
public bool SetErrorState(String message)
|
|
{
|
|
if (StringUtil.IsEmpty(message)) return true;
|
|
this.errorState = new SacsErrorState(message);
|
|
return false;
|
|
}
|
|
/// <summary>Clear the error state.</summary>
|
|
public void ClearErrorState()
|
|
{
|
|
errorState = null;
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
// 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);
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
// Response data.
|
|
// Null values are ignored by JSON serialisation.
|
|
// - See Saxonite.Net.Core.Web.Base.App.SaxStartup.ConfigureServices()
|
|
// - Ref. config.services.AddMvc(...).AddJsonOptions(options => { options.JsonSerializerOptions.IgnoreNullValues = true; });
|
|
//-------------------------------------------------------------------
|
|
// Base response data - for simple responses.
|
|
public class AjaxResponse_Base
|
|
{
|
|
// Response status.
|
|
public bool? Success { get; set; } = null;
|
|
public bool? Error { get; set; } = null;
|
|
public bool? SessionExpired { get; protected set; } = null;
|
|
public bool? GoHome { get; protected set; } = null;
|
|
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.IsBlank(text))
|
|
Message = new MessageData() { Title = title, Text = text };
|
|
return this;
|
|
}
|
|
// Confirmation message - to query user.
|
|
public class ConfirmMessageData : MessageData
|
|
{
|
|
public SaConfirmActions Actions { get; set; }
|
|
}
|
|
public ConfirmMessageData ConfirmMessage { get; private set; }
|
|
public AjaxResponse_Base SetConfirmMessage(String title, String text, SaConfirmActions actions = SaConfirmActions.YesNo)
|
|
{
|
|
if (!StringUtil.IsBlank(text))
|
|
ConfirmMessage = new ConfirmMessageData() { Title = title, Text = text, Actions = actions };
|
|
return this;
|
|
}
|
|
public bool? HasConfirmMessage { get { return ConfirmMessage != null ? true : null; } }
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
public class AjaxResponse_Data : AjaxResponse_Base
|
|
{
|
|
//---------------------------------------------------------------
|
|
// Construction.
|
|
protected AjaxResponse_Data(SaModel model, String messageTitle = "")
|
|
{
|
|
if (model.ErrorMessages.Any())
|
|
{
|
|
SetMessage(StringUtil.OnBlank(messageTitle, model.GetText("label.error", "Error")), model.ErrorMessages.Concat("", "\n"));
|
|
}
|
|
else if (model.WarningMessages.Any())
|
|
{
|
|
SetMessage(StringUtil.OnBlank(messageTitle, model.GetText("label.warning", "Warning")), model.WarningMessages.Concat("", "\n"));
|
|
}
|
|
else if (model.InfoMessages.Any())
|
|
{
|
|
SetNotification(StringUtil.OnBlank(messageTitle, model.GetText("label.notification", "Notificaton")), model.InfoMessages.Concat("", "\n"));
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
/// <summary>
|
|
/// Field values - automated population of list iten properties,
|
|
/// eg. in table cells with matching (prefixed) class name.
|
|
/// </summary>
|
|
public class FieldValuesData
|
|
{
|
|
// Item property values keyed to match field name.
|
|
public Dictionary<string, string> Value { get; } = new Dictionary<string, string>();
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
/// <summary>Item data - Common properties for data items.</summary>
|
|
public class ItemData_Base
|
|
{
|
|
// Common item properties.
|
|
public String Id { get; set; } = null;
|
|
private String notes;
|
|
public String Notes { get { return StringUtil.IsBlank(notes) ? null : notes; } set { notes = StringUtil.EmptyOnNull(value).Trim(); } }
|
|
public String StatusCode { get; set; } = null;
|
|
public String StatusDesc { get; set; } = null;
|
|
// Flags for common status values.
|
|
// Return true or null so that property is not serialised when false.
|
|
public bool? Awaiting { get { return StatusCode.EqualsCI(SacsSystem.GetStatusCode(SaStatus.Awaiting)) ? true : (bool?)null; } }
|
|
public bool? Active { get { return StatusCode.EqualsCI(SacsSystem.GetStatusCode(SaStatus.Active)) ? true : (bool?)null; } }
|
|
public bool? Inactive { get { return StatusCode.EqualsCI(SacsSystem.GetStatusCode(SaStatus.Inactive)) ? true : (bool?)null; } }
|
|
public bool? Deleted { get { return StatusCode.EqualsCI(SacsSystem.GetStatusCode(SaStatus.Deleted)) ? true : (bool?)null; } }
|
|
/// <summary>Created on date - YYYYMMDD</summary>
|
|
public String CreatedOn { get; set; }
|
|
/// <summary>Created on date - formatted per locale</summary>
|
|
public String CreatedOnFmt { get; set; }
|
|
/// <summary>Updated on date - YYYYMMDD</summary>
|
|
public String UpdatedOn { get; set; }
|
|
/// <summary>Updated on date - formatted per locale</summary>
|
|
public String UpdatedOnFmt { get; set; }
|
|
}
|
|
/// <summary>Contact Method properties.</summary>
|
|
public class ContactMethodData
|
|
{
|
|
public String Method { get; set; }
|
|
public String MethodCaption { get; set; }
|
|
public String Value { get; set; }
|
|
}
|
|
/// <summary>Address properties.</summary>
|
|
public class AddressData : ItemData_Base
|
|
{
|
|
public String Line1 { get; set; }
|
|
public String Line2 { get; set; }
|
|
public String Line3 { get; set; }
|
|
public String Town { get; set; }
|
|
public String RegionIdProvince { get; set; }
|
|
public String RegionNameProvince { get; set; }
|
|
public String RegionIdCountry { get; set; }
|
|
public String RegionNameCountry { get; set; }
|
|
public String PostalCode { get; set; }
|
|
}
|
|
/// <summary>
|
|
/// Item data with label and value.
|
|
/// </summary>
|
|
public class ItemLabelValue
|
|
{
|
|
public String Label { get; set; }
|
|
public String Value { get; set; }
|
|
}
|
|
/// <summary>
|
|
/// Item data with just Id and name.
|
|
/// <para>- Id is String to allow for obfuscated values.</para>
|
|
/// </summary>
|
|
public class ItemIdName
|
|
{
|
|
public ItemIdName() { }
|
|
public ItemIdName(SysId id, String name)
|
|
{
|
|
this.Id = $"{SysId.ToLong(id)}";
|
|
this.Name = name;
|
|
}
|
|
public ItemIdName(String id, String name)
|
|
{
|
|
this.Id = id;
|
|
this.Name = name;
|
|
}
|
|
public String Id { get; set; }
|
|
public String Name { get; set; }
|
|
}
|
|
/// <summary>Item code and desc values.</summary>
|
|
public class ItemCodeDesc
|
|
{
|
|
public ItemCodeDesc() { }
|
|
public ItemCodeDesc(String code, String desc)
|
|
{
|
|
this.Code = code;
|
|
this.Desc = desc;
|
|
}
|
|
public String Code { get; set; }
|
|
public String Desc { get; set; }
|
|
}
|
|
/// <summary>Statistics Caption/Value pair.</summary>
|
|
public class StatData : AjaxResponse_Base
|
|
{
|
|
public String Caption { get; set; }
|
|
public String Value { get; set; }
|
|
}
|
|
/// <summary>Statistics Group.</summary>
|
|
public class StatsGroupData : AjaxResponse_Base
|
|
{
|
|
public String Caption { get; set; }
|
|
public List<StatData> Stats { get; } = new List<StatData>();
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// Reference data.
|
|
|
|
// Tax codes.
|
|
public class TaxCodeData
|
|
{
|
|
public string Id { get; set; }
|
|
public string Code { get; set; }
|
|
public string Caption { get; set; }
|
|
public decimal Rate { get; set; }
|
|
}
|
|
|
|
// Units of Measure.
|
|
public class UomData
|
|
{
|
|
public string Id { get; set; }
|
|
public string Category { get; set; }
|
|
public string Type { get; set; }
|
|
public string Caption { get; set; }
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// 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.Count > 0 ? controlValues.Values.ToList() : null; } }
|
|
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.IsBlank(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 AjaxResponse_Data SetNotification(String title, String message)
|
|
{
|
|
Notification = new NotificationData() { Title = title, Message = message };
|
|
return this;
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// Paged data.
|
|
public class PagingData
|
|
{
|
|
public PagingData(SaModel 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(SaModel model)
|
|
{
|
|
Paging = new PagingData(model);
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
}
|
|
|
|
//-------------------------------------------------------------------
|
|
}
|
|
//=======================================================================
|
|
}
|