This commit is contained in:
2018-06-28 23:41:48 +00:00
commit 515bd37952
256 changed files with 29890 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
using Microsoft.Extensions.Logging;
namespace AyaNova.Util
{
/// <summary>
/// Shared logger
/// </summary>
internal static class ApplicationLogging
{
internal static ILoggerFactory LoggerFactory { get; set; }// = new LoggerFactory();
internal static ILogger CreateLogger<T>() => LoggerFactory.CreateLogger<T>();
internal static ILogger CreateLogger(string categoryName) => LoggerFactory.CreateLogger(categoryName);
}
}

View File

@@ -0,0 +1,286 @@
using System;
using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
using AyaNova.Models;
namespace AyaNova.Util
{
//Key generator controller
public static class AySchema
{
private static ILogger log;
private static AyContext ct;
/////////////////////////////////////////////////////////////////
/////////// CHANGE THIS ON NEW SCHEMA UPDATE ////////////////////
private const int DESIRED_SCHEMA_LEVEL = 8;
internal const long EXPECTED_COLUMN_COUNT = 69;
internal const long EXPECTED_INDEX_COUNT = 14;
/////////////////////////////////////////////////////////////////
static int startingSchema = -1;
public static int currentSchema = -1;
//check and update schema
public static void CheckAndUpdate(AyContext context, ILogger logger)
{
ct = context;
log = logger;
//Check if ayschemaversion table exists
bool aySchemaVersionExists = false;
using (var command = ct.Database.GetDbConnection().CreateCommand())
{
command.CommandText = "SELECT * FROM information_schema.tables WHERE table_name = 'aschemaversion'";
ct.Database.OpenConnection();
using (var result = command.ExecuteReader())
{
if (result.HasRows)
{
aySchemaVersionExists = true;
}
ct.Database.CloseConnection();
}
}
//Create schema table (v1)
if (!aySchemaVersionExists)
{
log.LogDebug("aschemaversion table not found, creating now");
//nope, no schema table, add it now and set to v1
using (var cm = ct.Database.GetDbConnection().CreateCommand())
{
ct.Database.OpenConnection();
cm.CommandText = "CREATE TABLE aschemaversion (schema INTEGER NOT NULL);";
cm.ExecuteNonQuery();
cm.CommandText = "insert into aschemaversion (schema) values (1);";
cm.ExecuteNonQuery();
ct.Database.CloseConnection();
startingSchema = 1;
currentSchema = 1;
}
}
else
{
//get current schema level
using (var cm = ct.Database.GetDbConnection().CreateCommand())
{
log.LogDebug("Fetching current schema version");
cm.CommandText = "SELECT schema FROM aschemaversion;";
ct.Database.OpenConnection();
using (var result = cm.ExecuteReader())
{
if (result.HasRows)
{
result.Read();
currentSchema = startingSchema = result.GetInt32(0);
ct.Database.CloseConnection();
log.LogDebug("AyaNova schema version is " + currentSchema.ToString());
}
else
{
ct.Database.CloseConnection();
throw new System.Exception("AyaNova->AySchema->CheckAndUpdate: Error reading schema version");
}
}
}
}
//Bail early no update?
if (currentSchema == DESIRED_SCHEMA_LEVEL)
{
log.LogDebug("Current schema is at required schema version " + currentSchema.ToString());
return;
}
log.LogInformation("AyaNova database needs to be updated from schema version {0} to version {1}", currentSchema, DESIRED_SCHEMA_LEVEL);
//************* SCHEMA UPDATES ******************
//////////////////////////////////////////////////
// USER table locale text and default data
if (currentSchema < 2)
{
LogUpdateMessage(log);
//create locale text tables
exec("CREATE TABLE alocale (id BIGSERIAL PRIMARY KEY, ownerid bigint not null, name varchar(255) not null, stock bool, created timestamp not null)");
exec("CREATE UNIQUE INDEX localename_idx ON alocale (name)");
exec("CREATE TABLE alocaleitem (id BIGSERIAL PRIMARY KEY, localeid bigint not null REFERENCES alocale (id), key text not null, display text not null)");
exec("CREATE INDEX localeitemlid_key_idx ON alocaleitem (localeid,key)");
//Prime the db with the default LOCALES
AyaNova.Biz.PrimeData.PrimeLocales(ct);
//Add user table
exec("CREATE TABLE auser (id BIGSERIAL PRIMARY KEY, created timestamp not null, ownerid bigint not null, name varchar(255) not null, " +
"login text not null, password text not null, salt text not null, roles integer not null, localeid bigint REFERENCES alocale (id), " +
"dlkey text, dlkeyexpire timestamp)");
//Prime the db with the default MANAGER account
AyaNova.Biz.PrimeData.PrimeManagerAccount(ct);
setSchemaLevel(++currentSchema);
}
//////////////////////////////////////////////////
//LICENSE table
if (currentSchema < 3)
{
LogUpdateMessage(log);
//Add user table
exec("CREATE TABLE alicense (id BIGSERIAL PRIMARY KEY, key text not null)");
setSchemaLevel(++currentSchema);
}
//////////////////////////////////////////////////
//WIDGET table for development testing
if (currentSchema < 4)
{
LogUpdateMessage(log);
//Add widget table
//id, text, longtext, boolean, currency,
exec("CREATE TABLE awidget (id BIGSERIAL PRIMARY KEY, ownerid bigint not null, name varchar(255) not null, created timestamp not null, " +
"startdate timestamp, enddate timestamp, dollaramount money, active bool, roles int4)");
setSchemaLevel(++currentSchema);
}
//////////////////////////////////////////////////
// FileAttachment table
if (currentSchema < 5)
{
LogUpdateMessage(log);
exec("CREATE TABLE afileattachment (id BIGSERIAL PRIMARY KEY, created timestamp not null, ownerid bigint not null," +
"attachtoobjectid bigint not null, attachtoobjecttype integer not null, " +
"storedfilename text not null, displayfilename text not null, contenttype text, notes text)");
//index required for ops that need to check if file already in db (delete, count refs etc)
exec("CREATE INDEX storedfilename_idx ON afileattachment (storedfilename);");
setSchemaLevel(++currentSchema);
}
//////////////////////////////////////////////////
//TAG tables
if (currentSchema < 6)
{
LogUpdateMessage(log);
exec("CREATE TABLE atag (id BIGSERIAL PRIMARY KEY, ownerid bigint not null, name varchar(35) not null, created timestamp not null)");
exec("CREATE UNIQUE INDEX tagname_idx ON atag (name);");
exec("CREATE TABLE atagmap (id BIGSERIAL PRIMARY KEY, created timestamp not null, ownerid bigint not null," +
"tagid bigint not null REFERENCES atag (id), tagtoobjectid bigint not null, tagtoobjecttype integer not null)");
setSchemaLevel(++currentSchema);
}
//////////////////////////////////////////////////
// OPS LRO tables
if (currentSchema < 7)
{
LogUpdateMessage(log);
exec("CREATE TABLE aopsjob (gid uuid PRIMARY KEY, ownerid bigint not null, name text not null, created timestamp not null, exclusive bool not null, " +
"startafter timestamp not null, jobtype integer not null, objectid bigint null, objecttype integer null, jobstatus integer not null, jobinfo text null)");
exec("CREATE TABLE aopsjoblog (gid uuid PRIMARY KEY, jobid uuid not null REFERENCES aopsjob (gid), created timestamp not null, statustext text not null)");
setSchemaLevel(++currentSchema);
}
//////////////////////////////////////////////////
//LICENSE table new columns
//TODO: DO I need this anymore???
if (currentSchema < 8)
{
LogUpdateMessage(log);
//Add license related stuff
exec("ALTER TABLE alicense ADD COLUMN dbid uuid");
exec("ALTER TABLE alicense ADD COLUMN LastFetchStatus integer");
exec("ALTER TABLE alicense ADD COLUMN LastFetchMessage text");
setSchemaLevel(++currentSchema);
}
//////////////////////////////////////////////////
// FUTURE
// if (currentSchema < 9)
// {
// LogUpdateMessage(log);
// setSchemaLevel(++currentSchema);
// }
//!!!!WARNING: BE SURE TO UPDATE THE DbUtil::PrepareDatabaseForSeeding WHEN NEW TABLES ADDED!!!!
log.LogInformation("Finished updating database schema to version {0}", currentSchema);
//*************************************************************************************
}//eofunction
private static void setSchemaLevel(int nCurrentSchema)
{
exec("UPDATE aschemaversion SET schema=" + nCurrentSchema.ToString());
}
//execute command query
private static void exec(string q)
{
using (var cm = ct.Database.GetDbConnection().CreateCommand())
{
ct.Database.OpenConnection();
cm.CommandText = q;
cm.ExecuteNonQuery();
ct.Database.CloseConnection();
}
}
private static void LogUpdateMessage(ILogger log)
{
log.LogDebug($"Updating database to schema version {currentSchema + 1}");
}
//eoclass
}
//eons
}

View File

@@ -0,0 +1,29 @@
namespace AyaNova.Util
{
/// <summary>
/// Version strings centrally located for convenience
/// </summary>
internal static class AyaNovaVersion
{
public static string VersionString
{
get
{
return "8.0.0-alpha.2018.6.6";
}
}
public static string FullNameAndVersion
{
get
{
return "AyaNova server v" + VersionString;
}
}
}//eoc
}//eons

View File

@@ -0,0 +1,65 @@
using System;
using System.Reflection;
using System.Linq;
namespace AyaNova.Util
{
internal static class CopyObject
{
/// <summary>
/// Copies the data of one object to another. The target object 'pulls' properties of the first.
/// This any matching properties are written to the target.
///
/// The object copy is a shallow copy only. Any nested types will be copied as
/// whole values rather than individual property assignments (ie. via assignment)
/// </summary>
/// <param name="source">The source object to copy from</param>
/// <param name="target">The object to copy to</param>
/// <param name="excludedProperties">A comma delimited list of properties that should not be copied</param>
/// <param name="memberAccess">Reflection binding access</param>
public static void Copy(object source, object target, string excludedProperties="", BindingFlags memberAccess = BindingFlags.Public | BindingFlags.Instance)
{
string[] excluded = null;
if (!string.IsNullOrEmpty(excludedProperties))
excluded = excludedProperties.Split(new char[1] { ',' }, StringSplitOptions.RemoveEmptyEntries);
MemberInfo[] miT = target.GetType().GetMembers(memberAccess);
foreach (MemberInfo Field in miT)
{
string name = Field.Name;
// Skip over any property exceptions
if (!string.IsNullOrEmpty(excludedProperties) &&
excluded.Contains(name))
continue;
if (Field.MemberType == MemberTypes.Field)
{
FieldInfo SourceField = source.GetType().GetField(name);
if (SourceField == null)
continue;
object SourceValue = SourceField.GetValue(source);
((FieldInfo)Field).SetValue(target, SourceValue);
}
else if (Field.MemberType == MemberTypes.Property)
{
PropertyInfo piTarget = Field as PropertyInfo;
PropertyInfo SourceField = source.GetType().GetProperty(name, memberAccess);
if (SourceField == null)
continue;
if (piTarget.CanWrite && SourceField.CanRead)
{
object SourceValue = SourceField.GetValue(source, null);
piTarget.SetValue(target, SourceValue, null);
}
}
}
}
}//eoc
}//eons

View File

@@ -0,0 +1,69 @@
using System;
namespace AyaNova.Util
{
internal static class DateUtil
{
/// <summary>
/// Is the current date after the referenced date by at least the duration specified
/// </summary>
/// <param name="startDate">UTC start point to compare to current UTC date</param>
/// <param name="Hours"></param>
/// <param name="Minutes"></param>
/// <param name="Seconds"></param>
/// <returns></returns>
public static bool IsAfterDuration(DateTime startDate, int Hours, int Minutes = 0, int Seconds = 0)
{
TimeSpan ts = new TimeSpan(Hours, Minutes, Seconds);
return IsAfterDuration(startDate, ts);
}
/// <summary>
/// Is the current date after the referenced date by at least the timespan specified
/// </summary>
/// <param name="startDate">UTC start point to compare to current UTC date</param>
/// <param name="tspan"></param>
/// <returns></returns>
public static bool IsAfterDuration(DateTime startDate, TimeSpan tspan)
{
if (DateTime.UtcNow - startDate < tspan)
return false;
return true;
}
/// <summary>
/// An internally consistent empty or not relevant date marker:
/// January 1st 5555
/// </summary>
/// <returns></returns>
public static DateTime EmptyDateValue
{
get
{
return new DateTime(5555, 1, 1);
//Was going to use MaxValue but apparently that varies depending on culture
// and Postgres has issues with year 1 as it interprets as year 2001
// so to be on safe side just defining one for all usage
}
}
/// <summary>
/// returns a UTC short date, short time formatted date for local display to end user in logs, errors etc at the server level
/// (Not related to UI display of dates and times)
/// </summary>
/// <param name="DateToDisplay"></param>
/// <returns></returns>
public static string ServerDateTimeString(DateTime DateToDisplay)
{
return DateToDisplay.ToLocalTime().ToString("g");
}
}//eoc
}//eons

View File

@@ -0,0 +1,480 @@
using System;
using Microsoft.Extensions.Logging;
using AyaNova.Models;
using System.Linq;
using System.Collections.Generic;
namespace AyaNova.Util
{
internal static class DbUtil
{
private static string _RawAyaNovaConnectionString;
private static string _dbConnectionString;
private static string _dbName;
private static string _dbUserName;
private static string _dbPassword;
private static string _dbServer;
#region parse connection string
internal static void ParseConnectionString(ILogger _log, string AyaNovaConnectionString)
{
if (string.IsNullOrWhiteSpace(AyaNovaConnectionString))
{
_log.LogDebug("There is no database server connection string set, AYANOVA_DB_CONNECTION is missing or empty. Will use default: \"Server=localhost;Username=postgres;Database=AyaNova;\"");
AyaNovaConnectionString = "Server=localhost;Username=postgres;Database=AyaNova;";
}
_RawAyaNovaConnectionString = AyaNovaConnectionString;
var builder = new System.Data.Common.DbConnectionStringBuilder();
builder.ConnectionString = AyaNovaConnectionString;
if (!builder.ContainsKey("database"))
{
_log.LogDebug("There is no database name specified (\"Database=<NAME>\") in connection string. Will use default: \"Database=AyaNova;\"");
builder.Add("database", "AyaNova");
}
//Keep track of default values
_dbConnectionString = builder.ConnectionString;
if (builder.ContainsKey("database"))
_dbName = builder["database"].ToString();
if (builder.ContainsKey("username"))
_dbUserName = builder["username"].ToString();
if (builder.ContainsKey("password"))
_dbPassword = builder["password"].ToString();
if (builder.ContainsKey("server"))
_dbServer = builder["server"].ToString();
_log.LogDebug("AyaNova will use the following connection string: {0}", PasswordRedactedConnectionString(_dbConnectionString));
}
///////////////////////////////////////////
//clean out password from connection string
//for log purposes
private static string PasswordRedactedConnectionString(string cs)
{
var nStart = 0;
var nStop = 0;
var lwrcs = cs.ToLowerInvariant();
nStart = lwrcs.IndexOf("password");
if (nStart == -1)
{
//no password, just return it
return cs;
}
//find terminating semicolon
nStop = lwrcs.IndexOf(";", nStart);
if (nStop == -1 || nStop == lwrcs.Length)
{
//no terminating semicolon or that is the final character in the string
return cs.Substring(0, nStart + 9) + "[redacted];";
}
else
{
//not the last thing in the string so return the whole string minus the password part
return cs.Substring(0, nStart + 9) + "[redacted];" + cs.Substring(nStop + 1);
}
}
#endregion
#region Connection utilities
///////////////////////////////////////////
//Verify that server exists
//
private static string AdminConnectionString
{
get
{
return _dbConnectionString.Replace(_dbName, "postgres");
}
}
///////////////////////////////////////////
//Connection string without password
//
internal static string DisplayableConnectionString
{
get
{
return PasswordRedactedConnectionString(_dbConnectionString);
}
}
#endregion
#region DB verification
///////////////////////////////////////////
//Verify that server exists
//
internal static bool DatabaseServerExists(ILogger log, string logPrepend)
{
try
{
//Try every 3 seconds for 10 tries before giving up
var maxRetryAttempts = 10;
var pauseBetweenFailures = TimeSpan.FromSeconds(3);
RetryHelper.RetryOnException(maxRetryAttempts, pauseBetweenFailures, log, logPrepend + AdminConnectionString, () =>
{
using (var conn = new Npgsql.NpgsqlConnection(AdminConnectionString))
{
conn.Open();
conn.Close();
}
});
}
catch
{
return false;
}
return true;
}
///////////////////////////////////////////
//Verify that database exists, if not, then create it
//
internal static bool EnsureDatabaseExists(ILogger _log)
{
_log.LogDebug("Ensuring database exists. Connection string is: \"{0}\"", DisplayableConnectionString);
using (var conn = new Npgsql.NpgsqlConnection(_dbConnectionString))
{
try
{
conn.Open();
conn.Close();
}
catch (Exception e)
{
//if it's a db doesn't exist that's ok, we'll create it, not an error
if (e is Npgsql.PostgresException)
{
if (((Npgsql.PostgresException)e).SqlState == "3D000")
{
//create the db here
using (var cnCreate = new Npgsql.NpgsqlConnection(AdminConnectionString))
{
cnCreate.Open();
// Create the database desired
using (var cmd = new Npgsql.NpgsqlCommand())
{
cmd.Connection = cnCreate;
cmd.CommandText = "CREATE DATABASE \"" + _dbName + "\";";
cmd.ExecuteNonQuery();
_log.LogInformation("Database \"{0}\" created successfully!", _dbName);
}
cnCreate.Close();
}
}
else
{
var err = string.Format("Database server connection failed. Connection string is: \"{0}\"", DisplayableConnectionString);
_log.LogCritical(e, "BOOT: E1000 - " + err);
err = err + "\nError reported was: " + e.Message;
throw new ApplicationException(err);
}
}
}
}
return true;
}
#endregion
#region DB utilities
///////////////////////////////////////////
// Drop and re-create db
// This is the NUCLEAR option and
// completely ditches the DB and all user uploaded files
//
internal static void DropAndRecreateDb(ILogger _log)
{
_log.LogInformation("Dropping and recreating Database \"{0}\"", _dbName);
//clear all connections so that the database can be dropped
Npgsql.NpgsqlConnection.ClearAllPools();
using (var conn = new Npgsql.NpgsqlConnection(AdminConnectionString))
{
conn.Open();
// Create the database desired
using (var cmd = new Npgsql.NpgsqlCommand())
{
cmd.Connection = conn;
cmd.CommandText = "DROP DATABASE \"" + _dbName + "\";";
cmd.ExecuteNonQuery();
cmd.Connection = conn;
cmd.CommandText = "CREATE DATABASE \"" + _dbName + "\";";
cmd.ExecuteNonQuery();
_log.LogInformation("Database re-created successfully!");
}
conn.Close();
}
//final cleanup step is to erase user uploaded files
FileUtil.EraseEntireContentsOfUserFilesFolder();
}
/////////////////////////////////////////////////////////
// Erase all user entered data from the db
// This is called by seeder for trial seeding purposes
//
internal static void PrepareDatabaseForSeeding(ILogger _log)
{
_log.LogInformation("Erasing Database \"{0}\"", _dbName);
AyaNova.Api.ControllerHelpers.ApiServerState apiServerState = (AyaNova.Api.ControllerHelpers.ApiServerState)ServiceProviderProvider.Provider.GetService(typeof(AyaNova.Api.ControllerHelpers.ApiServerState));
apiServerState.SetClosed("Erasing database");
//clear all connections so that the database can be dropped
Npgsql.NpgsqlConnection.ClearAllPools();
using (var conn = new Npgsql.NpgsqlConnection(_dbConnectionString))
{
conn.Open();
using (var cmd = new Npgsql.NpgsqlCommand())
{
cmd.Connection = conn;
cmd.CommandText = "delete from \"auser\" where id <> 1;";
cmd.ExecuteNonQuery();
}
//THIS METHOD IS ONLY CALLED BY SEEDER
//SO ONLY REMOVE DATA THAT IS SEEDED
//I.E. Normal user business data, not infrastructure data like license or localized text etc
EraseTable("atagmap", conn);
EraseTable("atag", conn);
EraseTable("afileattachment", conn);
EraseTable("awidget", conn);
conn.Close();
}
//If we got here then it's safe to erase the attachment files
FileUtil.EraseEntireContentsOfUserFilesFolder();
apiServerState.ResumePriorState();
}
///////////////////////////////////////////
// Erase all data from the table specified
//
private static void EraseTable(string sTable, Npgsql.NpgsqlConnection conn)
{
using (var cmd = new Npgsql.NpgsqlCommand())
{
cmd.Connection = conn;
cmd.CommandText = "TRUNCATE \"" + sTable + "\" RESTART IDENTITY CASCADE;";
cmd.ExecuteNonQuery();
}
}
///////////////////////////////////////////
// Check if DB is empty
//
internal static bool DBIsEmpty(AyContext ctx, ILogger _log)
{
//TODO: This needs to be way more thorough, only the main tables though, no need to get crazy with it
//just stuff that would be shitty to have to re-enter
_log.LogDebug("DB empty check");
//An empty db contains only one User
if (ctx.User.Count() > 1) return false;
//No clients
//if(ctx.Client.Count()>0) return false;
//No units
//if(ctx.Unit.Count()>0) return false;
//No parts
//if(ctx.Part.Count()>0) return false;
//No workorders
//if(ctx.Workorder.Count()>0) return false;
return true;
}
///////////////////////////////////////////
// Ensure the db is not modified
//
internal static void CheckFingerPrint(long ExpectedColumns, long ExpectedIndexes, ILogger _log)
{
_log.LogDebug("Checking DB integrity");
long actualColumns = 0;
long actualIndexes = 0;
using (var conn = new Npgsql.NpgsqlConnection(_dbConnectionString))
{
conn.Open();
using (var command = conn.CreateCommand())
{
//Count all columns in all our tables
command.CommandText = "SELECT count(*) FROM information_schema.columns where table_schema='public'";
using (var result = command.ExecuteReader())
{
if (result.HasRows)
{
//check the values
result.Read();
actualColumns = result.GetInt64(0);
}
else
{
var err = "E1030 - Database integrity check failed, could not obtain column data. Contact support.";
_log.LogCritical(err);
throw new ApplicationException(err);
}
}
}
using (var command = conn.CreateCommand())
{
//Count all indexes in all our tables
command.CommandText = "select Count(*) from pg_indexes where schemaname='public'";
using (var result = command.ExecuteReader())
{
if (result.HasRows)
{
//check the values
result.Read();
actualIndexes = result.GetInt64(0);
}
else
{
var err = "E1030 - Database integrity check failed, could not obtain index data. Contact support.";
_log.LogCritical(err);
throw new ApplicationException(err);
}
}
}
conn.Close();
if (ExpectedColumns != actualColumns || ExpectedIndexes != actualIndexes)
{
var err = string.Format("E1030 - Database integrity check failed (C{0}I{1})", actualColumns, actualIndexes);
_log.LogCritical(err);
throw new ApplicationException(err);
}
}
}
///////////////////////////////////////////
// Given a table name return the count of records in that table
// Used for metrics
//
///
internal static long CountOfRecords(string TableName)
{
long ret = 0;
using (var conn = new Npgsql.NpgsqlConnection(_dbConnectionString))
{
conn.Open();
using (var command = conn.CreateCommand())
{
command.CommandText = $"SELECT count(*) FROM {TableName}";
using (var result = command.ExecuteReader())
{
if (result.HasRows)
{
result.Read();
ret = result.GetInt64(0);
}
}
}
conn.Close();
}
return ret;
}
///////////////////////////////////////////
// Returns all table names that are ours in current schema
//
///
internal static List<string> GetAllTablenames()
{
List<string> ret = new List<string>();
using (var conn = new Npgsql.NpgsqlConnection(_dbConnectionString))
{
conn.Open();
using (var command = conn.CreateCommand())
{
command.CommandText = "SELECT table_name FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE';";
using (var result = command.ExecuteReader())
{
if (result.HasRows)
{
while (result.Read())
{
ret.Add(result.GetString(0));
}
}
}
}
conn.Close();
}
return ret;
}
#endregion
}//eoc
}//eons

View File

@@ -0,0 +1,36 @@
using System;
/// <summary>
/// Get custom attribute extension
/// </summary>
public static class EnumExtension
{
/// <summary>
/// Check if enum has attribute type
/// Example usage bool c = Biz.AyaType.License.HasAttribute(typeof(Biz.AttachableAttribute));
///
/// </summary>
/// <param name="value"></param>
/// <param name="t"></param>
/// <returns></returns>
public static bool HasAttribute(this Enum value, Type t)
{
var type = value.GetType();
var name = Enum.GetName(type, value);
if (name != null)
{
var field = type.GetField(name);
if (field != null)
{
var attr =
Attribute.GetCustomAttribute(field, t);
if (attr != null)
{
return true;
}
}
}
return false;
}
}

View File

@@ -0,0 +1,32 @@
using System;
using System.Text;
namespace AyaNova.Util
{
internal static class ExceptionUtil
{
/// <summary>
/// Extract and return exception message
/// Handles innermost exceptions level by level
/// </summary>
/// <param name="ex"></param>
/// <returns></returns>
public static string ExtractAllExceptionMessages(Exception ex)
{
StringBuilder sb = new StringBuilder();
while (ex != null)
{
sb.AppendLine($"{ex.Source} -> {ex.Message}");
ex = ex.InnerException;
}
return sb.ToString();
}
}//eoc
}//eons

View File

@@ -0,0 +1,25 @@
using System.IO;
using System.Security.Cryptography;
using System;
namespace AyaNova.Util
{
internal static class FileHash
{
internal static string GetChecksum(string filePath)
{
using (FileStream stream = File.OpenRead(filePath))
{
SHA256Managed sha = new SHA256Managed();
byte[] checksum = sha.ComputeHash(stream);
return BitConverter.ToString(checksum).Replace("-", String.Empty);
}
}
}//eoc
}//eons

View File

@@ -0,0 +1,470 @@
using System;
using System.IO;
using System.IO.Compression;
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json.Linq;
using AyaNova.Models;
using AyaNova.Biz;
using System.Linq;
namespace AyaNova.Util
{
/*
- Quickly generate large files in windows: http://tweaks.com/windows/62755/quickly-generate-large-test-files-in-windows/
*/
internal static class FileUtil
{
#region Folder ensurance
/// <summary>
/// Ensurs folders exist and are not identical
/// Throws an exception of they are found to be identical preventing startup
/// The reason for this is to prevent a future erase database operation (which erases all attachment files)
/// from erasing backups which might prevent recovery in case someone accidentally erases their database
/// </summary>
/// <param name="contentRootPath"></param>
/// <returns></returns>
internal static void EnsureUserAndUtilityFoldersExistAndAreNotIdentical(string contentRootPath)
{
//UserFiles
if (string.IsNullOrWhiteSpace(ServerBootConfig.AYANOVA_FOLDER_USER_FILES))
{
ServerBootConfig.AYANOVA_FOLDER_USER_FILES = Path.Combine(contentRootPath, "userfiles");
}
//BackupFiles
if (ServerBootConfig.AYANOVA_FOLDER_BACKUP_FILES == null)
{
ServerBootConfig.AYANOVA_FOLDER_BACKUP_FILES = Path.Combine(contentRootPath, "backupfiles");
}
//Prevent using the same folder for both
if (string.Equals(Path.GetFullPath(ServerBootConfig.AYANOVA_FOLDER_USER_FILES), Path.GetFullPath(ServerBootConfig.AYANOVA_FOLDER_BACKUP_FILES), StringComparison.OrdinalIgnoreCase))
{
throw new System.NotSupportedException("E1040: The configuration settings AYANOVA_FOLDER_USER_FILES and the AYANOVA_FOLDER_BACKUP_FILES must not point to the exact same location");
}
EnsurePath(ServerBootConfig.AYANOVA_FOLDER_USER_FILES);
EnsurePath(ServerBootConfig.AYANOVA_FOLDER_BACKUP_FILES);
}
//create path if doesn't exist already
private static void EnsurePath(string path)
{
if (!Directory.Exists(path))
Directory.CreateDirectory(path);
}
#endregion folder ensurance
#region Utility file handling
/// <summary>
/// Get a path combining supplied file name and backup files folder
/// </summary>
/// <returns></returns>
internal static string GetFullPathForUtilityFile(string fileName)
{
return Path.Combine(UtilityFilesFolder, fileName);
}
/// <summary>
/// Get backup folder
/// </summary>
/// <returns></returns>
internal static string UtilityFilesFolder
{
get
{
return ServerBootConfig.AYANOVA_FOLDER_BACKUP_FILES;
}
}
/// <summary>
/// Delete a utility file (backup folder file)
/// </summary>
/// <param name="fileName"></param>
internal static void DeleteUtilityFile(string fileName)
{
var utilityFilePath = GetFullPathForUtilityFile(fileName);
if (File.Exists(utilityFilePath))
{
File.Delete(utilityFilePath);
}
}
/// <summary>
/// Get a list of files in the utility folder
///
/// </summary>
/// <param name="searchPattern">search pattern for files desired, leave blank for any </param>
/// <returns></returns>
internal static List<string> UtilityFileList(string searchPattern = "*")
{
List<string> returnList = new List<string>();
foreach (string file in Directory.EnumerateFiles(UtilityFilesFolder, searchPattern))
{
returnList.Add(Path.GetFileName(file));
}
returnList.Sort();
return returnList;
}
/// <summary>
/// Confirm if a file exists in the utility folder
/// </summary>
/// <param name="fileName">name of utility file </param>
/// <returns>duh!</returns>
internal static bool UtilityFileExists(string fileName)
{
if (string.IsNullOrWhiteSpace(fileName))
return false;
var utilityFilePath = GetFullPathForUtilityFile(fileName);
return File.Exists(utilityFilePath);
}
#endregion Utility file handling
#region Zip handling
////////////////////////////////////////////////////////////////////////////////////////
//ZIP handling
/// <summary>
/// Get zip entries for a utlity file
/// </summary>
/// <param name="zipFileName"></param>
/// <returns></returns>
internal static List<string> ZipGetUtilityFileEntries(string zipFileName)
{
return ZipGetEntries(GetFullPathForUtilityFile(zipFileName));
}
/// <summary>
/// Get zip entries for full path and file name
/// returns the entry fullname sorted alphabetically so that folders stay together
/// </summary>
/// <param name="zipPath"></param>
/// <returns></returns>
internal static List<string> ZipGetEntries(string zipPath)
{
List<string> zipEntries = new List<string>();
using (ZipArchive archive = ZipFile.OpenRead(zipPath))
{
foreach (ZipArchiveEntry entry in archive.Entries)
{
zipEntries.Add(entry.FullName);
}
}
zipEntries.Sort();
return zipEntries;
}
/// <summary>
/// Import utility - get individual files specified in zip archive as JSON objects
///
/// </summary>
/// <param name="zipFileName">Name of utility zip import file</param>
/// <param name="entryList">Name of entries in utility file archive to fetch</param>
/// <returns></returns>
internal static List<JObject> ZipGetUtilityArchiveEntriesAsJsonObjects(List<string> entryList, string zipFileName)
{
List<JObject> jList = new List<JObject>();
var zipPath = GetFullPathForUtilityFile(zipFileName);
using (ZipArchive archive = ZipFile.OpenRead(zipPath))
{
foreach (string importFileName in entryList)
{
ZipArchiveEntry entry = archive.GetEntry(importFileName);
if (entry != null)
{
//stream entry into a new jobject and add it to the list
StreamReader reader = new StreamReader(entry.Open());
string text = reader.ReadToEnd();
var j = JObject.Parse(text);
//Here add v7 import file name as sometimes it's needed later (locales)
j.Add("V7_SOURCE_FILE_NAME", JToken.FromObject(importFileName));
jList.Add(j);
}
}
}
return jList;
}
#endregion Zip handling
#region Attachment file handling
/// <summary>
/// Get user folder
/// </summary>
/// <returns></returns>
internal static string UserFilesFolder
{
get
{
return ServerBootConfig.AYANOVA_FOLDER_USER_FILES;
}
}
/// <summary>
/// Get a random file name
/// </summary>
/// <returns></returns>
internal static string NewRandomFileName
{
get
{
return Path.GetRandomFileName();
}
}
/// <summary>
/// Get a random file name with path to attachments folder
/// </summary>
/// <returns></returns>
internal static string NewRandomAttachmentFileName
{
get
{
return Path.Combine(UserFilesFolder, NewRandomFileName);
}
}
/// <summary>
/// Store a file attachment
/// </summary>
/// <param name="tempFilePath"></param>
/// <param name="contentType"></param>
/// <param name="fileName"></param>
/// <param name="userId"></param>
/// <param name="attachToObject"></param>
/// <param name="ct"></param>
/// <returns></returns>
internal static FileAttachment storeFileAttachment(string tempFilePath, string contentType, string fileName, long userId, AyaTypeId attachToObject, AyContext ct)
{
//calculate hash
var hash = FileHash.GetChecksum(tempFilePath);
//Move to folder based on hash
var permanentPath = GetPermanentAttachmentPath(hash);
EnsurePath(permanentPath);
var permanentFilePath = Path.Combine(permanentPath, hash);
//See if the file was already uploaded, if so then ignore it for now
if (File.Exists(permanentFilePath))
{
//delete the temp file, it's already stored
File.Delete(tempFilePath);
}
else
{
System.IO.File.Move(tempFilePath, permanentFilePath);
}
//Build AyFileInfo
FileAttachment fi = new FileAttachment()
{
OwnerId = userId,
StoredFileName = hash,
DisplayFileName = fileName,
Notes = string.Empty,
ContentType = contentType,
AttachToObjectId = attachToObject.ObjectId,
AttachToObjectType = attachToObject.ObjectType
};
//Store in DB
ct.FileAttachment.Add(fi);
ct.SaveChanges();
//Return AyFileInfo object
return fi;
}
/// <summary>
///use first three characters for name of folders one character per folder, i.e.:
///if the checksum is f6a5b1236dbba1647257cc4646308326
///it would be stored in userfiles/f/6/a/f6a5b1236dbba1647257cc4646308326
/// </summary>
/// <param name="hash"></param>
/// <returns>Path without the file</returns>
internal static string GetPermanentAttachmentPath(string hash)
{
return Path.Combine(UserFilesFolder, hash[0].ToString(), hash[1].ToString(), hash[2].ToString());
}
/// <summary>
/// Get the whole path including file name not just the folder
/// </summary>
/// <param name="hash"></param>
/// <returns></returns>
internal static string GetPermanentAttachmentFilePath(string hash)
{
return Path.Combine(UserFilesFolder, hash[0].ToString(), hash[1].ToString(), hash[2].ToString(), hash);
}
/// <summary>
/// Delete a file attachment
/// checks ref count and if would be zero deletes file physically
/// otherwise just deletes pointer in db
/// </summary>
/// <param name="fileAttachmentToBeDeleted"></param>
/// <param name="ct"></param>
/// <returns></returns>
internal static FileAttachment deleteFileAttachment(FileAttachment fileAttachmentToBeDeleted, AyContext ct)
{
//check ref count of file
var count = ct.FileAttachment.Count(w => w.StoredFileName == fileAttachmentToBeDeleted.StoredFileName);
//Store in DB
ct.FileAttachment.Remove(fileAttachmentToBeDeleted);
ct.SaveChanges();
if (count < 2)
{
//remove the file completely
var permanentPath = GetPermanentAttachmentPath(fileAttachmentToBeDeleted.StoredFileName);
var permanentFilePath = Path.Combine(permanentPath, fileAttachmentToBeDeleted.StoredFileName);
if (File.Exists(permanentFilePath))
{
//delete the temp file, it's already stored
File.Delete(permanentFilePath);
}
}
//Return AyFileInfo object
return fileAttachmentToBeDeleted;
}
/// <summary>
/// DANGER: Erases all user files
/// </summary>
internal static void EraseEntireContentsOfUserFilesFolder()
{
System.IO.DirectoryInfo di = new DirectoryInfo(UserFilesFolder);
foreach (FileInfo file in di.EnumerateFiles())
{
file.Delete();
}
foreach (DirectoryInfo dir in di.EnumerateDirectories())
{
dir.Delete(true);
}
}
#endregion attachment stuff
#region General utilities
/// <summary>
/// Attachments / user files folder size info
/// </summary>
/// <returns></returns>
internal static FolderSizeInfo GetAttachmentFolderSizeInfo()
{
return GetDirectorySize(new DirectoryInfo(UserFilesFolder));
}
/// <summary>
/// Utility / backup folder file size info
/// </summary>
/// <returns></returns>
internal static FolderSizeInfo GetUtilityFolderSizeInfo()
{
return GetDirectorySize(new DirectoryInfo(UtilityFilesFolder));
}
/// <summary>
/// Calculate disk space usage under <paramref name="root"/>. If <paramref name="levels"/> is provided,
/// then return subdirectory disk usages as well, up to <paramref name="levels"/> levels deep.
/// If levels is not provided or is 0, return a list with a single element representing the
/// directory specified by <paramref name="root"/>.
///
/// FROM https://stackoverflow.com/a/28094795/8939
///
/// </summary>
/// <returns></returns>
public static FolderSizeInfo GetDirectorySize(DirectoryInfo root, int levels = 0)
{
var currentDirectory = new FolderSizeInfo();
// Add file sizes.
FileInfo[] fis = root.GetFiles();
currentDirectory.Size = 0;
foreach (FileInfo fi in fis)
{
currentDirectory.Size += fi.Length;
}
// Add subdirectory sizes.
DirectoryInfo[] dis = root.GetDirectories();
currentDirectory.Path = root;
currentDirectory.SizeWithChildren = currentDirectory.Size;
currentDirectory.DirectoryCount = dis.Length;
currentDirectory.DirectoryCountWithChildren = dis.Length;
currentDirectory.FileCount = fis.Length;
currentDirectory.FileCountWithChildren = fis.Length;
if (levels >= 0)
currentDirectory.Children = new List<FolderSizeInfo>();
foreach (DirectoryInfo di in dis)
{
var dd = GetDirectorySize(di, levels - 1);
if (levels >= 0)
currentDirectory.Children.Add(dd);
currentDirectory.SizeWithChildren += dd.SizeWithChildren;
currentDirectory.DirectoryCountWithChildren += dd.DirectoryCountWithChildren;
currentDirectory.FileCountWithChildren += dd.FileCountWithChildren;
}
return currentDirectory;
}
public class FolderSizeInfo
{
public DirectoryInfo Path { get; set; }
public long SizeWithChildren { get; set; }
public long Size { get; set; }
public int DirectoryCount { get; set; }
public int DirectoryCountWithChildren { get; set; }
public int FileCount { get; set; }
public int FileCountWithChildren { get; set; }
public List<FolderSizeInfo> Children { get; set; }
}
#endregion general utilities
}//eoc
}//eons

View File

@@ -0,0 +1,53 @@
using System;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
namespace AyaNova.Util
{
public static class Hasher
{
public static string hash(string Salt, string Password)
{
//adapted from here:
//https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/consumer-apis/password-hashing
string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2(
password: Password,
salt: Convert.FromBase64String(Salt),
prf: KeyDerivationPrf.HMACSHA512,
iterationCount: 10000,
numBytesRequested: 512 / 8));
return hashed;
}
//Generate salt
public static string GenerateSalt()
{
var salt = new byte[32];
var random = RandomNumberGenerator.Create();
random.GetNonZeroBytes(salt);
return Convert.ToBase64String(salt);
}
// /// <summary>
// /// Generate a random ID
// /// </summary>
// /// <returns>HEX</returns>
// internal static string GenerateStrongId()
// {
// var s = new byte[32];
// var random = RandomNumberGenerator.Create();
// random.GetNonZeroBytes(s);
// return BitConverter.ToString(s).Replace("-", string.Empty).ToLowerInvariant();
// }
}//eoc
}//eons

View File

@@ -0,0 +1,31 @@
using System.Net;
using Microsoft.AspNetCore.Http;
//https://stackoverflow.com/a/41242493/8939
public static class IsLocalExtension
{
private const string NullIpAddress = "::1";
public static bool IsLocal(this HttpRequest req)
{
var connection = req.HttpContext.Connection;
if (connection.RemoteIpAddress.IsSet())
{
//We have a remote address set up
return connection.LocalIpAddress.IsSet()
//Is local is same as remote, then we are local
? connection.RemoteIpAddress.Equals(connection.LocalIpAddress)
//else we are remote if the remote IP address is not a loopback address
: IPAddress.IsLoopback(connection.RemoteIpAddress);
}
return true;
}
private static bool IsSet(this IPAddress address)
{
return address != null && address.ToString() != NullIpAddress;
}
}

View File

@@ -0,0 +1,645 @@
using System;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Collections.Generic;
using System.Net.Http;
using AyaNova.Util;
using AyaNova.Models;
using System.Linq;
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
//JSON KEY
using Newtonsoft.Json;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.OpenSsl;
//using System.Security.Cryptography;
//using Microsoft.AspNetCore.Cryptography.KeyDerivation;
namespace AyaNova.Core
{
internal static class License
{
//License server address
private const string LICENSE_SERVER_URL = "https://rockfish.ayanova.com/";
// private const string LICENSE_SERVER_URL = "http://localhost:5000/";
//Scheduleable users
private const string SERVICE_TECHS_FEATURE_NAME = "ServiceTechs";
//Accounting add-on
private const string ACCOUNTING_FEATURE_NAME = "Accounting";
//This feature name means it's a trial key
private const string TRIAL_FEATURE_NAME = "TrialMode";
//This feature name means it's a SAAS or rental mode key for month to month hosted service
private const string RENTAL_FEATURE_NAME = "ServiceMode";
//Trial key magic number for development and testing, all other guids will be fully licensed
private static Guid TEST_TRIAL_KEY_DBID = new Guid("{A6D18A8A-5613-4979-99DA-80D07641A2FE}");
//https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/
private static HttpClient _Client = new HttpClient();
//Current license key, can be empty
private static AyaNovaLicenseKey _ActiveLicense = new AyaNovaLicenseKey();
//The one and only DBID
private static Guid DbId { get; set; }
#region license classes
//DTO object returned on license query
internal class LicenseFeature
{
//name of feature / product
public string Feature { get; set; }
//Optional count for items that require it
public long Count { get; set; }
}
//DTO object for parsed key
internal class AyaNovaLicenseKey
{
public AyaNovaLicenseKey()
{
Features = new List<LicenseFeature>();
RegisteredTo = "UNLICENSED";
Id = RegisteredTo;
}
/// <summary>
/// Fetch the license status of the feature in question
/// </summary>
/// <param name="Feature"></param>
/// <returns>LicenseFeature object or null if there is no license</returns>
public LicenseFeature GetLicenseFeature(string Feature)
{
if (IsEmpty)
return null;
string lFeature = Feature.ToLowerInvariant();
foreach (LicenseFeature l in Features)
{
if (l.Feature.ToLowerInvariant() == lFeature)
{
return l;
}
}
return null;
}
/// <summary>
/// Check for the existance of license feature
/// </summary>
/// <param name="Feature"></param>
/// <returns>bool</returns>
public bool HasLicenseFeature(string Feature)
{
if (IsEmpty)
return false;
string lFeature = Feature.ToLowerInvariant();
foreach (LicenseFeature l in Features)
{
if (l.Feature.ToLowerInvariant() == lFeature)
{
return true;
}
}
return false;
}
public bool IsEmpty
{
get
{
//Key is empty if it's not registered to anyone or there are no features in it
return string.IsNullOrWhiteSpace(RegisteredTo) || (Features == null || Features.Count == 0);
}
}
public bool WillExpire
{
get
{
return LicenseExpiration < DateUtil.EmptyDateValue;
}
}
public bool LicenseExpired
{
get
{
return LicenseExpiration < DateTime.Now;
}
}
public bool MaintenanceExpired
{
get
{
return MaintenanceExpiration < DateTime.Now;
}
}
public bool TrialLicense
{
get
{
return HasLicenseFeature(TRIAL_FEATURE_NAME);
}
}
public bool RentalLicense
{
get
{
return HasLicenseFeature(RENTAL_FEATURE_NAME);
}
}
public string LicenseFormat { get; set; }
public string Id { get; set; }
public string RegisteredTo { get; set; }
public Guid DbId { get; set; }
public DateTime LicenseExpiration { get; set; }
public DateTime MaintenanceExpiration { get; set; }
public List<LicenseFeature> Features { get; set; }
}
#endregion
#region sample v8 key
// private static string SAMPLE_KEY = @"[KEY
// {
// ""Key"": {
// ""LicenseFormat"": ""2018"",
// ""Id"": ""34-1516288681"", <----Customer id followed by key serial id
// ""RegisteredTo"": ""Super TestCo"",
// ""DBID"": ""df558559-7f8a-4c7b-955c-959ebcdf71f3"",
// ""LicenseExpiration"": ""2019-01-18T07:18:01.2329138-08:00"", <--- UTC, DateTime if perpetual license 1/1/5555 indicates not expiring
// ""MaintenanceExpiration"": ""2019-01-18T07:18:01.2329138-08:00"", <-- UTC, DateTime support and updates subscription runs out, applies to all features
// ""Features"": {
// ""Feature"": [
// {
// ""Name"": ""Scheduleable users"",
// ""Count"":""10"",
// },
// {
// ""Name"": ""Accounting""
// },
// {
// ""Name"": ""TrialMode""<---means is a trial key
// },
// {
// ""Name"": ""ServiceMode"" <----Means it's an SAAS/Rental key
// }
// ]
// }
// }
// }
// KEY]
// [SIGNATURE
// HEcY3JCVwK9HFXEFnldUEPXP4Q7xoZfMZfOfx1cYmfVF3MVWePyZ9dqVZcY7pk3RmR1BbhQdhpljsYLl+ZLTRhNa54M0EFa/bQnBnbwYZ70EQl8fz8WOczYTEBo7Sm5EyC6gSHtYZu7yRwBvhQzpeMGth5uWnlfPb0dMm0DQM7PaqhdWWW9GCSOdZmFcxkFQ8ERLDZhVMbd8PJKyLvZ+sGMrmYTAIoL0tqa7nrxYkM71uJRTAmQ0gEl4bJdxiV825U1J+buNQuTZdacZKEPSjQQkYou10jRbReUmP2vDpvu+nA1xdJe4b5LlyQL+jiIXH17lf93xlCUb0UkDpu8iNQ==
// SIGNATURE]\";
#endregion
#region Exposed properties
/// <summary>
/// Fetch a summary of the license key for displaying to the end user
/// </summary>
/// <returns> string containing current license information</returns>
internal static string LicenseInfo
{
get
{
StringBuilder sb = new StringBuilder();
if (ActiveKey.IsEmpty)
{
sb.AppendLine("UNLICENSED");
sb.AppendLine($"DB ID: {DbId}");
}
else
{
if (ActiveKey.TrialLicense)
sb.AppendLine("TRIAL LICENSE FOR EVALUATION PURPOSES ONLY");
sb.AppendLine($"Registered to: {ActiveKey.RegisteredTo}");
sb.AppendLine($"Key ID: {ActiveKey.Id}");
sb.AppendLine($"DB ID: {DbId}");
sb.AppendLine($"Type: {(ActiveKey.RentalLicense ? "Service" : "Perpetual")}");
if (ActiveKey.WillExpire)
sb.AppendLine($"License expires: {DateUtil.ServerDateTimeString(ActiveKey.LicenseExpiration)}");
sb.AppendLine($"Maintenance subscription expires: {DateUtil.ServerDateTimeString(ActiveKey.MaintenanceExpiration)}");
sb.AppendLine("Features:");
foreach (LicenseFeature l in ActiveKey.Features)
{
//don't show the rental or trial features
if (l.Feature != TRIAL_FEATURE_NAME && l.Feature != RENTAL_FEATURE_NAME)
{
if (l.Count != 0)
sb.AppendLine($"{l.Feature} - {l.Count}");
else
sb.AppendLine($"{l.Feature}");
}
}
}
return sb.ToString();
}
}
/// <summary>
/// Fetch a summary of the license key for displaying to the end user
/// via the API
/// </summary>
/// <returns> JSON object containing current license information</returns>
internal static object LicenseInfoAsJson
{
get
{
Newtonsoft.Json.Linq.JObject o = Newtonsoft.Json.Linq.JObject.FromObject(new
{
license = new
{
licensedTo = ActiveKey.RegisteredTo,
dbId = ActiveKey.DbId,
keySerial = ActiveKey.Id,
licenseExpiration = ActiveKey.LicenseExpiration,
maintenanceExpiration = ActiveKey.MaintenanceExpiration,
features =
from f in ActiveKey.Features
orderby f.Feature
select new
{
Feature = f.Feature,
Count = f.Count
}
}
});
return o;
}
}
/// <summary>
/// Returns the active key
/// </summary>
/// <returns></returns>
internal static AyaNovaLicenseKey ActiveKey
{
get
{
return _ActiveLicense;
}
}
#endregion
#region Trial license request handling
/// <summary>
/// Request a key
/// </summary>
/// <returns>Result string</returns>
internal static string RequestTrial(string email, string regto, ILogger log)
{
//TODO: TESTING REMOVE BEFORE RELEASE
//for test purposes if this route is hit and this code executed then the dbid is temporarily changed to the special trial request
//dbid so I can test remotely without hassle
//TO USE: just hit the trial key request route once then the license fetch route and it should be easy peasy
log.LogCritical("WARNING License::RequestTrial - DEVELOPMENT TEST FORCING TRIAL DB KEY ID. UPDATE BEFORE RELEASE!!");
DbId=TEST_TRIAL_KEY_DBID;
//TESTING
Microsoft.AspNetCore.Http.Extensions.QueryBuilder q = new Microsoft.AspNetCore.Http.Extensions.QueryBuilder();
q.Add("dbid", DbId.ToString());
q.Add("email", email);
q.Add("regto", regto);
log.LogDebug($"Requesting trial license for DBID {DbId.ToString()}");
string sUrl = $"{LICENSE_SERVER_URL}rvr" + q.ToQueryString();
try
{
var res = _Client.GetStringAsync(sUrl).Result;
return res;
}
catch (Exception ex)
{
var msg = "E1020 - Error requesting trial license key see log for details";
log.LogError(ex, msg);
return msg;
// throw new ApplicationException(msg, ex);
}
}
#endregion trial license request handling
#region License fetching and handling
/// <summary>
/// Fetch a key, validate it and install it in the db then initialize with it
/// </summary>
/// <returns>Result string</returns>
internal static void Fetch(AyaNova.Api.ControllerHelpers.ApiServerState apiServerState, AyContext ctx, ILogger log)
{
log.LogDebug($"Fetching license for DBID {DbId.ToString()}");
string sUrl = $"{LICENSE_SERVER_URL}rvf/{DbId.ToString()}";
#if (DEBUG)
log.LogInformation("DEBUG MODE TRIAL LICENSE KEY BEING FETCHED");
sUrl = $"{LICENSE_SERVER_URL}rvf/{TEST_TRIAL_KEY_DBID.ToString()}";
#endif
try
{
string RawTextKeyFromRockfish = _Client.GetStringAsync(sUrl).Result;
//FUTURE: if there is any kind of error response or REASON or LicenseFetchStatus then here is
//where to deal with it
AyaNovaLicenseKey ParsedKey = Parse(RawTextKeyFromRockfish, log);
if (ParsedKey != null)
{
Install(RawTextKeyFromRockfish, ParsedKey, apiServerState, ctx, log);
}
}
catch (Exception ex)
{
var msg = "E1020 - Error fetching license key";
log.LogError(ex, msg);
throw new ApplicationException(msg, ex);
}
}
/// <summary>
/// Initialize the key
/// Handle if first boot scenario to tag DB ID etc
/// </summary>
internal static void Initialize(AyaNova.Api.ControllerHelpers.ApiServerState apiServerState, AyContext ctx, ILogger log)
{
log.LogDebug("Initializing license");
try
{
//Fetch key from db as no tracking so doesn't hang round if need to immediately clear and then re-add the key
Models.License ldb = ctx.License.AsNoTracking().SingleOrDefault();
//Non existent license should restrict server to ops routes only with closed API
if (ldb == null)
{
ldb = new Models.License();
ldb.DbId = Guid.NewGuid();
ldb.LastFetchStatus = 0;
ldb.Key = "none";
ldb.LastFetchMessage = "none";
ctx.License.Add(ldb);
ctx.SaveChanges();
}
//ensure DB ID
if (ldb.DbId == Guid.Empty)
{
ldb.DbId = Guid.NewGuid();
//Convert the no tracking record fetched above to tracking
//this is required because a prior call to initialize before dumping the db would mean the license is still in memory in the context
ctx.Entry(ldb).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
ctx.SaveChanges();
}
//Get it early and set it here so that it can be displayed early to the user even if not licensed
DbId = ldb.DbId;
if (ldb.Key == "none")
{
var msg = "License key not found in database, running in unlicensed mode";
apiServerState.SetSystemLock(msg);
log.LogWarning(msg);
return;
}
//Validate the key
AyaNovaLicenseKey k = Parse(ldb.Key, log);
if (k == null)
{
var msg = "Error: License key in database is not valid, running in unlicensed mode";
apiServerState.SetSystemLock(msg);
log.LogError(msg);
return;
}
_ActiveLicense = k;
if (_ActiveLicense.LicenseExpired)
{
var msg = $"License key expired {DateUtil.ServerDateTimeString(_ActiveLicense.LicenseExpiration)}";
apiServerState.SetSystemLock(msg);
log.LogWarning(msg);
return;
}
//Key is ok, might not have been on first boot so check and clear if locked
//This works for now because system lock only means license lock
//if ever changed for other purposes then need to handle that see serverstate for ideas
if (apiServerState.IsSystemLocked)
{
apiServerState.ClearSystemLock();
}
log.LogDebug("License key OK");
}
catch (Exception ex)
{
var msg = "E1020 - Error initializing license key";
log.LogError(ex, msg);
throw new ApplicationException(msg, ex);
}
}
/// <summary>
/// Install key to db
/// </summary>
private static bool Install(string RawTextNewKey, AyaNovaLicenseKey ParsedNewKey, AyaNova.Api.ControllerHelpers.ApiServerState apiServerState, AyContext ctx, ILogger log)
{
try
{
var CurrentInDbKeyRecord = ctx.License.FirstOrDefault();
if (CurrentInDbKeyRecord == null)
throw new ApplicationException("E1020 - Can't install key, no key record found");
if (ParsedNewKey == null)
{
throw new ApplicationException("License.Install -> key could not be parsed");
}
//Can't install a trial into a non-empty db
if (ParsedNewKey.TrialLicense && !DbUtil.DBIsEmpty(ctx, log))
{
throw new ApplicationException("E1020 - Can't install a trial key into a non empty AyaNova database. Erase the database first.");
}
//Update current license
CurrentInDbKeyRecord.Key = RawTextNewKey;
//TODO: reason, resultcode etc
ctx.SaveChanges();
}
catch (Exception ex)
{
var msg = "E1020 - Error installing license key";
log.LogError(ex, msg);
throw new ApplicationException(msg, ex);
}
finally
{
Initialize(apiServerState, ctx, log);
}
return true;
}
#endregion
#region PARSE and Validate key
/// <summary>
/// Parses and validates the integrity of a passed in textual license key
/// </summary>
/// <returns>a populated key if valid or else null</returns>
private static AyaNovaLicenseKey Parse(string k, ILogger log)
{
AyaNovaLicenseKey key = new AyaNovaLicenseKey();
log.LogDebug("Validating license");
if (string.IsNullOrWhiteSpace(k))
{
throw new ApplicationException("License.Parse -> License key is empty and can't be validated");
}
try
{
if (!k.Contains("[KEY") ||
!k.Contains("KEY]") ||
!k.Contains("[SIGNATURE") ||
!k.Contains("SIGNATURE]"))
{
throw new ApplicationException("License.Parse -> License key is missing required delimiters");
}
string keyNoWS = System.Text.RegularExpressions.Regex.Replace(StringUtil.Extract(k, "[KEY", "KEY]").Trim(), "(\"(?:[^\"\\\\]|\\\\.)*\")|\\s+", "$1");
string keySig = StringUtil.Extract(k, "[SIGNATURE", "SIGNATURE]").Trim();
#region Check Signature
//***** NOTE: this is our real 2016 public key
var publicPem = @"-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz7wrvLDcKVMZ31HFGBnL
WL08IodYIV5VJkKy1Z0n2snprhSiu3izxTyz+SLpftvKHJpky027ii7l/pL9Bo3J
cjU5rKrxXavnE7TuYPjXn16dNLd0K/ERSU+pXLmUaVN0nUWuGuUMoGJMEXoulS6p
JiG11yu3BM9fL2Nbj0C6a+UwzEHFmns3J/daZOb4gAzMUdJfh9OJ0+wRGzR8ZxyC
99Na2gDmqYglUkSMjwLTL/HbgwF4OwmoQYJBcET0Wa6Gfb17SaF8XRBV5ZtpCsbS
tkthGeoXZkFriB9c1eFQLKpBYQo2DW3H1MPG2nAlQZLbkJj5cSh7/t1bRF08m6P+
EQIDAQAB
-----END PUBLIC KEY-----";
PemReader pr = new PemReader(new StringReader(publicPem));
var KeyParameter = (Org.BouncyCastle.Crypto.AsymmetricKeyParameter)pr.ReadObject();
var signer = SignerUtilities.GetSigner("SHA256WITHRSA");
signer.Init(false, KeyParameter);
var expectedSig = Convert.FromBase64String(keySig);
var msgBytes = Encoding.UTF8.GetBytes(keyNoWS);
signer.BlockUpdate(msgBytes, 0, msgBytes.Length);
if (!signer.VerifySignature(expectedSig))
{
throw new ApplicationException("License.Parse -> License key failed integrity check and is not valid");
}
#endregion check signature
#region Get Values
Newtonsoft.Json.Linq.JToken token = Newtonsoft.Json.Linq.JObject.Parse(keyNoWS);
key.LicenseFormat = (string)token.SelectToken("Key.LicenseFormat");
if (key.LicenseFormat != "2018")
throw new ApplicationException($"License.Parse -> License key format {key.LicenseFormat} not recognized");
key.Id = (string)token.SelectToken("Key.Id");
key.RegisteredTo = (string)token.SelectToken("Key.RegisteredTo");
key.DbId = (Guid)token.SelectToken("Key.DBID");
key.LicenseExpiration = (DateTime)token.SelectToken("Key.LicenseExpiration");
key.MaintenanceExpiration = (DateTime)token.SelectToken("Key.MaintenanceExpiration");
//FEATURES
Newtonsoft.Json.Linq.JArray p = (Newtonsoft.Json.Linq.JArray)token.SelectToken("Key.Features");
for (int x = 0; x < p.Count; x++)
{
LicenseFeature lf = new LicenseFeature();
lf.Feature = (string)p[x].SelectToken("Name");
if (p[x].SelectToken("Count") != null)
{
lf.Count = (long)p[x].SelectToken("Count");
}
else
{
lf.Count = 0;
}
key.Features.Add(lf);
}
#endregion get values
//All is well return key
return key;
}
catch (Exception ex)
{
var msg = "E1020 - License key not valid";
log.LogError(ex, msg);
throw new ApplicationException(msg, ex);
}
}
#endregion
}//eoc
}//eons

View File

@@ -0,0 +1,167 @@
using App.Metrics;
using App.Metrics.Counter;
using App.Metrics.Gauge;
using App.Metrics.Histogram;
using App.Metrics.ReservoirSampling.Uniform;
using App.Metrics.Meter;
using App.Metrics.Timer;
using App.Metrics.Apdex;
using App.Metrics.ReservoirSampling.ExponentialDecay;
namespace AyaNova.Util
{
/// <summary>
/// All metrics gathered by AyaNova are defined here
/// (except for endpoint ones gathered automatically by App.Metrics)
/// https://www.app-metrics.io
/// </summary>
public static class MetricsRegistry
{
/// <summary>
/// Physical memory
/// Memory being used by this process (RAVEN)
/// </summary>
public static GaugeOptions PhysicalMemoryGauge = new GaugeOptions
{
Name = "Process Physical Memory",
MeasurementUnit = Unit.Bytes
};
/// <summary>
/// Private bytes
/// The current size, in bytes, of the committed memory owned by this process.
/// Memory leaks are identified by a consistent and prolonged increase in Private Bytes.
/// This is the best performance counter for detecting memory leaks.
/// </summary>
public static GaugeOptions PrivateBytesGauge = new GaugeOptions
{
Name = "Process Private Bytes",
MeasurementUnit = Unit.Bytes
};
/// <summary>
/// Exceptions that are handled by the ApiCustomExceptionFilter
/// Basically any exception that is not normal and expected
/// </summary>
public static MeterOptions UnhandledExceptionsMeter => new MeterOptions
{
Name = "Exceptions Meter",
MeasurementUnit = Unit.Calls
};
/// <summary>
/// Login failed meter
/// </summary>
public static MeterOptions FailedLoginMeter => new MeterOptions
{
Name = "Failed Login Meter",
MeasurementUnit = Unit.Calls
};
/// <summary>
/// Login failed meter
/// </summary>
public static MeterOptions SuccessfulLoginMeter => new MeterOptions
{
Name = "Successful Login Meter",
MeasurementUnit = Unit.Calls
};
/// <summary>
/// Records in db
/// </summary>
public static GaugeOptions DBRecordsGauge = new GaugeOptions
{
Name = "DB Records",
MeasurementUnit = Unit.Items
};
/// <summary>
/// Jobs in db
/// </summary>
public static GaugeOptions JobsGauge = new GaugeOptions
{
Name = "Jobs",
MeasurementUnit = Unit.Items
};
/// <summary>
/// File count on disk
/// </summary>
public static GaugeOptions FileCountGauge = new GaugeOptions
{
Name = "File count",
MeasurementUnit = Unit.Items
};
/// <summary>
/// File size on disk
/// </summary>
public static GaugeOptions FileSizeGauge = new GaugeOptions
{
Name = "File size",
MeasurementUnit = Unit.Bytes
};
// ==================================================================
// /// <summary>
// ///
// /// </summary>
// public static GaugeOptions Errors => new GaugeOptions
// {
// Context = "My_Gauge_context",
// Name = "Errors"
// };
// /// <summary>
// ///
// /// </summary>
// public static HistogramOptions SampleHistogram => new HistogramOptions
// {
// Name = "Sample Histogram",
// Reservoir = () => new DefaultAlgorithmRReservoir(),
// MeasurementUnit = Unit.MegaBytes
// };
// /// <summary>
// ///
// /// </summary>
// public static MeterOptions SampleMeter => new MeterOptions
// {
// Name = "Sample Meter",
// MeasurementUnit = Unit.Calls
// };
// /// <summary>
// ///
// /// </summary>
// public static TimerOptions SampleTimer => new TimerOptions
// {
// Name = "Sample Timer",
// MeasurementUnit = Unit.Items,
// DurationUnit = TimeUnit.Milliseconds,
// RateUnit = TimeUnit.Milliseconds,
// Reservoir = () => new DefaultForwardDecayingReservoir(sampleSize: 1028, alpha: 0.015)
// };
// /// <summary>
// ///
// /// </summary>
// public static ApdexOptions SampleApdex => new ApdexOptions
// {
// Name = "Sample Apdex"
// };
}
}

View File

@@ -0,0 +1,53 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace AyaNova.Util
{
/// <summary>
/// what it says
/// </summary>
public static class RetryHelper
{
//private static ILog logger = LogManager.GetLogger(); //use a logger or trace of your choice
// private readonly ILogger log;
/// <summary>
///
/// </summary>
/// <param name="times"></param>
/// <param name="delay"></param>
/// <param name="log"></param>
/// <param name="logPrepend"></param>
/// <param name="operation"></param>
public static void RetryOnException(int times, TimeSpan delay, ILogger log, string logPrepend, Action operation)
{
var attempts = 0;
do
{
try
{
attempts++;
operation();
break; // Sucess! Lets exit the loop!
}
catch (Exception ex)
{
if (attempts == times)
throw;
log.LogError(ex, $"{logPrepend} Exception caught on attempt {attempts} - will retry after delay {delay}");
Task.Delay(delay).Wait();
}
} while (true);
}
}
}

View File

@@ -0,0 +1,286 @@
using System;
using AyaNova.Models;
using AyaNova.Biz;
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
using Bogus;
using AyaNova.Api.ControllerHelpers;
namespace AyaNova.Util
{
public static class Seeder
{
public enum SeedLevel { SmallOneManShopTrialDataSet, MediumLocalServiceCompanyTrialDataSet, LargeCorporateMultiRegionalTrialDataSet };
// //////////////////////////////////////////////////////
// //Seed database with default manager account
// //
// public static User GenerateDefaultManagerAccountUser()
// {
// User u = new User();
// u.Name = "AyaNova Administrator";
// u.Salt = Hasher.GenerateSalt();
// u.Login = "manager";
// u.Password = Hasher.hash(u.Salt, "l3tm3in");
// u.Roles = AuthorizationRoles.BizAdminFull | AuthorizationRoles.OpsAdminFull | AuthorizationRoles.DispatchFull | AuthorizationRoles.InventoryFull;
// u.OwnerId = 1;
// return u;
// }
//////////////////////////////////////////////////////
//Seed database for trial and testing purposes
//
public static void SeedDatabase(AyContext ct, SeedLevel slevel)
{
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger("Seeder");
ApiServerState apiServerState = (ApiServerState)ServiceProviderProvider.Provider.GetService(typeof(ApiServerState));
//get the current server state so can set back to it later
ApiServerState.ServerState wasServerState = apiServerState.GetState();
string wasReason=apiServerState.Reason;
try
{
log.LogInformation("SEEDER: SeedDatabase, level is: " + slevel.ToString());
//Only allow this in a trial database
if (!AyaNova.Core.License.ActiveKey.TrialLicense)
{
throw new System.NotSupportedException("This database has a registered license key and can't be seeded.");
}
log.LogInformation("Setting server state to OpsOnly");
apiServerState.SetOpsOnly("Seeding database");
//Erase all the data except for the license, schema and the manager user
DbUtil.PrepareDatabaseForSeeding(log);
var f = new Faker("en");
//Seed special test data for integration testing
SeedTestData(ct);
switch (slevel)
{
//This is for a busy but one man shop with a single office person handling stuff back at the shop
case SeedLevel.SmallOneManShopTrialDataSet:
//Generate owner and lead tech
GenSeedUser(1, ct, AuthorizationRoles.BizAdminFull | AuthorizationRoles.DispatchFull | AuthorizationRoles.InventoryFull | AuthorizationRoles.OpsAdminFull);
//Generate one office person / secretary
GenSeedUser(1, ct, AuthorizationRoles.DispatchFull | AuthorizationRoles.InventoryFull | AuthorizationRoles.AccountingFull);
//200 widgets
GenSeedWidget(200, ct);
break;
//This is for a typical AyaNova medium busy shop
//has one location, many techs and full staff for each department
case SeedLevel.MediumLocalServiceCompanyTrialDataSet:
//One IT administrator, can change ops but nothing else
GenSeedUser(1, ct, AuthorizationRoles.OpsAdminFull);
//One business administrator, can view ops issues
GenSeedUser(1, ct, AuthorizationRoles.BizAdminFull | AuthorizationRoles.OpsAdminLimited);
//One owner who doesn't control anything but views stuff
GenSeedUser(1, ct, AuthorizationRoles.DispatchLimited | AuthorizationRoles.InventoryLimited | AuthorizationRoles.OpsAdminLimited);
//20 techs
GenSeedUser(20, ct, AuthorizationRoles.TechFull | AuthorizationRoles.DispatchLimited);
//2 subcontractors
GenSeedUser(2, ct, AuthorizationRoles.SubContractorFull);
//3 sales / generic office people people
GenSeedUser(3, ct, AuthorizationRoles.DispatchLimited | AuthorizationRoles.InventoryLimited);
//1 dispatch manager
GenSeedUser(1, ct, AuthorizationRoles.DispatchFull | AuthorizationRoles.InventoryLimited);
//1 Inventory manager
GenSeedUser(1, ct, AuthorizationRoles.InventoryFull | AuthorizationRoles.DispatchLimited);
//1 accountant / bookkeeper
GenSeedUser(1, ct, AuthorizationRoles.AccountingFull | AuthorizationRoles.BizAdminLimited);
//10 full on client users
GenSeedUser(10, ct, AuthorizationRoles.ClientLimited);
//10 limited client users
GenSeedUser(10, ct, AuthorizationRoles.ClientLimited);
//2000 widgets
GenSeedWidget(2000, ct);
break;
//this is a large corporation with multiple branches in multiple locations all in the same country
//Each location has a full staff and corporate head office has an overarching staff member in charge of each location
case SeedLevel.LargeCorporateMultiRegionalTrialDataSet:
//IT administrator, can change ops but nothing else
GenSeedUser(2, ct, AuthorizationRoles.OpsAdminFull);
//business administrator, can view ops issues
GenSeedUser(2, ct, AuthorizationRoles.BizAdminFull | AuthorizationRoles.OpsAdminLimited);
//owner / upper management who doesn't control anything but views stuff
GenSeedUser(5, ct, AuthorizationRoles.DispatchLimited | AuthorizationRoles.InventoryLimited | AuthorizationRoles.OpsAdminLimited);
//techs
GenSeedUser(100, ct, AuthorizationRoles.TechFull | AuthorizationRoles.DispatchLimited);
//limited techs
GenSeedUser(50, ct, AuthorizationRoles.TechLimited | AuthorizationRoles.DispatchLimited);
//20 subcontractors
GenSeedUser(20, ct, AuthorizationRoles.SubContractorFull);
//10 limited subcontractors
GenSeedUser(10, ct, AuthorizationRoles.SubContractorLimited);
//30 sales / generic office people people
GenSeedUser(30, ct, AuthorizationRoles.DispatchLimited | AuthorizationRoles.InventoryLimited);
//5 dispatch manager
GenSeedUser(5, ct, AuthorizationRoles.DispatchFull | AuthorizationRoles.InventoryLimited);
//5 Inventory manager
GenSeedUser(5, ct, AuthorizationRoles.InventoryFull | AuthorizationRoles.DispatchLimited);
//10 Inventory manager assistants
GenSeedUser(5, ct, AuthorizationRoles.InventoryLimited);
//5 accountant / bookkeeper
GenSeedUser(5, ct, AuthorizationRoles.AccountingFull | AuthorizationRoles.BizAdminLimited);
//100 full on client users
GenSeedUser(100, ct, AuthorizationRoles.ClientFull);
//100 limited client users
GenSeedUser(100, ct, AuthorizationRoles.ClientLimited);
//20000 widgets
GenSeedWidget(20000, ct);
break;
}
log.LogInformation("Seeding completed successfully");
}
catch
{
throw;
}
finally
{
log.LogInformation($"Seeder: setting server state back to {wasServerState.ToString()}");
apiServerState.SetState(wasServerState, wasReason);
}
}
//////////////////////////////////////////////////////
//Seed test data for integration tests
//
public static void SeedTestData(AyContext ct)
{
//TEST USERS
//one of each role type
GenSeedUser(1, ct, AuthorizationRoles.BizAdminLimited, "BizAdminLimited", "BizAdminLimited");
GenSeedUser(1, ct, AuthorizationRoles.BizAdminFull, "BizAdminFull", "BizAdminFull");
GenSeedUser(1, ct, AuthorizationRoles.DispatchLimited, "DispatchLimited", "DispatchLimited");
GenSeedUser(1, ct, AuthorizationRoles.DispatchFull, "DispatchFull", "DispatchFull");
GenSeedUser(1, ct, AuthorizationRoles.InventoryLimited, "InventoryLimited", "InventoryLimited");
GenSeedUser(1, ct, AuthorizationRoles.InventoryFull, "InventoryFull", "InventoryFull");
GenSeedUser(1, ct, AuthorizationRoles.AccountingFull, "Accounting", "Accounting");
GenSeedUser(1, ct, AuthorizationRoles.TechLimited, "TechLimited", "TechLimited");
GenSeedUser(1, ct, AuthorizationRoles.TechFull, "TechFull", "TechFull");
GenSeedUser(1, ct, AuthorizationRoles.SubContractorLimited, "SubContractorLimited", "SubContractorLimited");
GenSeedUser(1, ct, AuthorizationRoles.SubContractorFull, "SubContractorFull", "SubContractorFull");
GenSeedUser(1, ct, AuthorizationRoles.ClientLimited, "ClientLimited", "ClientLimited");
GenSeedUser(1, ct, AuthorizationRoles.ClientFull, "ClientFull", "ClientFull");
GenSeedUser(1, ct, AuthorizationRoles.OpsAdminLimited, "OpsAdminLimited", "OpsAdminLimited");
GenSeedUser(1, ct, AuthorizationRoles.OpsAdminFull, "OpsAdminFull", "OpsAdminFull");
//PRIVACY TEST USER - this is used for a test to see if user info leaks into the logs
GenSeedUser(1, ct, AuthorizationRoles.OpsAdminLimited, "TEST_PRIVACY_USER_ACCOUNT", "TEST_PRIVACY_USER_ACCOUNT");
}
//////////////////////////////////////////////////////
//Seed user - default login / pw is first name
//
public static void GenSeedUser(int count, AyContext ct, AuthorizationRoles roles, string login = null, string password = null)
{
for (int x = 0; x < count; x++)
{
User u = new User();
var p = new Bogus.Person();
u.Name = p.FullName;
u.Salt = Hasher.GenerateSalt();
if (login != null)
{
u.Login = login;
u.Name += " - " + login;
}
else
u.Login = p.FirstName;
if (password != null)
u.Password = Hasher.hash(u.Salt, password);
else
u.Password = Hasher.hash(u.Salt, u.Login);
u.Roles = roles;
u.LocaleId=ServerBootConfig.AYANOVA_DEFAULT_LANGUAGE_ID;
ct.User.Add(u);
}
ct.SaveChanges();
}
//////////////////////////////////////////////////////
//Seed widget for testing
//
public static void GenSeedWidget(int count, AyContext ct)
{
for (int x = 0; x < count; x++)
{
Widget o = new Widget();
var f = new Bogus.Faker();
o.Name = f.Commerce.ProductName();
o.Active = f.Random.Bool();
o.StartDate = f.Date.Between(DateTime.Now, DateTime.Now.AddMinutes(60));
o.EndDate = f.Date.Between(DateTime.Now.AddMinutes(90), DateTime.Now.AddHours(5));
o.DollarAmount = Convert.ToDecimal(f.Commerce.Price());
o.OwnerId = 1;
//this is nonsense but just to test an enum
o.Roles = AuthorizationRoles.DispatchLimited | AuthorizationRoles.InventoryLimited | AuthorizationRoles.OpsAdminLimited;
ct.Widget.Add(o);
}
ct.SaveChanges();
}
}//eoc
}//eons

View File

@@ -0,0 +1,190 @@
using System;
using System.IO;
using Microsoft.Extensions.Configuration;
namespace AyaNova.Util
{
/// <summary>
/// Contains config values from bootup
/// </summary>
internal static class ServerBootConfig
{
//CONTENTROOTPATH
internal static string AYANOVA_CONTENT_ROOT_PATH { get; set; } //Note: set in startup.cs, not in program.cs as it requires startup IHostingEnvironment
//LANGUAGE / LOCALE
internal static string AYANOVA_DEFAULT_LANGUAGE { get; set; }
internal static long AYANOVA_DEFAULT_LANGUAGE_ID { get; set; } //internal setting set at boot by LocaleBiz::ValidateLocales
//API
internal static string AYANOVA_JWT_SECRET { get; set; }
internal static string AYANOVA_USE_URLS { get; set; }
//DATABASE
internal static string AYANOVA_DB_CONNECTION { get; set; }
internal static bool AYANOVA_PERMANENTLY_ERASE_DATABASE { get; set; }
//FILE FOLDERS
internal static string AYANOVA_FOLDER_USER_FILES { get; set; }
internal static string AYANOVA_FOLDER_BACKUP_FILES { get; set; }
//LOGGING
internal static string AYANOVA_LOG_PATH { get; set; }
internal static string AYANOVA_LOG_LEVEL { get; set; }
internal static bool AYANOVA_LOG_ENABLE_LOGGER_DIAGNOSTIC_LOG { get; set; }
//METRICS
internal static bool AYANOVA_METRICS_USE_INFLUXDB { get; set; }
internal static string AYANOVA_METRICS_INFLUXDB_BASEURL { get; set; }
internal static string AYANOVA_METRICS_INFLUXDB_DBNAME { get; set; }
internal static string AYANOVA_METRICS_INFLUXDB_CONSISTENCY { get; set; }
internal static string AYANOVA_METRICS_INFLUXDB_USERNAME { get; set; }
internal static string AYANOVA_METRICS_INFLUXDB_PASSWORD { get; set; }
internal static string AYANOVA_METRICS_INFLUXDB_RETENSION_POLICY { get; set; }
internal static bool AYANOVA_METRICS_INFLUXDB_CREATE_DATABASE_IF_NOT_EXISTS { get; set; }
/// <summary>
/// Populate the config from the configuration found at boot
/// called by program.cs
/// </summary>
/// <param name="config"></param>
internal static void SetConfiguration(IConfigurationRoot config)
{
bool? bTemp = null;
#region SERVER BASICS
//LANGUAGE
//LocaleBiz will validate this later at boot pfc and ensure a sane default is set (English)
AYANOVA_DEFAULT_LANGUAGE = config.GetValue<string>("AYANOVA_DEFAULT_LANGUAGE");
AYANOVA_DEFAULT_LANGUAGE = string.IsNullOrWhiteSpace(AYANOVA_DEFAULT_LANGUAGE) ? "en" : AYANOVA_DEFAULT_LANGUAGE;
string lowLocale = AYANOVA_DEFAULT_LANGUAGE.ToLowerInvariant();
switch (lowLocale)
{
case "en":
case "english":
AYANOVA_DEFAULT_LANGUAGE = "en";
break;
case "de":
case "deutsch":
case "german":
AYANOVA_DEFAULT_LANGUAGE = "de";
break;
case "es":
case "español":
case "spanish":
AYANOVA_DEFAULT_LANGUAGE = "es";
break;
case "fr":
case "français":
case "french":
AYANOVA_DEFAULT_LANGUAGE = "fr";
break;
default:
AYANOVA_DEFAULT_LANGUAGE = "en";
break;
}
//LOGLEVEL
AYANOVA_LOG_LEVEL = config.GetValue<string>("AYANOVA_LOG_LEVEL");
AYANOVA_LOG_LEVEL = string.IsNullOrWhiteSpace(AYANOVA_LOG_LEVEL) ? "Info" : AYANOVA_LOG_LEVEL;
//LOGGING DIAGNOSTIC LOG
bTemp = config.GetValue<bool?>("AYANOVA_LOG_ENABLE_LOGGER_DIAGNOSTIC_LOG");
AYANOVA_LOG_ENABLE_LOGGER_DIAGNOSTIC_LOG = (null == bTemp) ? false : (bool)bTemp;
//PORT / API
AYANOVA_USE_URLS = config.GetValue<string>("AYANOVA_USE_URLS");
AYANOVA_USE_URLS = string.IsNullOrWhiteSpace(AYANOVA_USE_URLS) ? "http://*:7575" : AYANOVA_USE_URLS;
AYANOVA_JWT_SECRET = config.GetValue<string>("AYANOVA_JWT_SECRET");
//DB
AYANOVA_DB_CONNECTION = config.GetValue<string>("AYANOVA_DB_CONNECTION");
bTemp = config.GetValue<bool?>("AYANOVA_PERMANENTLY_ERASE_DATABASE");
AYANOVA_PERMANENTLY_ERASE_DATABASE = (null == bTemp) ? false : (bool)bTemp;
//FOLDERS
//Log folder
AYANOVA_LOG_PATH = config.GetValue<string>("AYANOVA_LOG_PATH");
if (AYANOVA_LOG_PATH == null)
{
//DEFAULT LOG PATH
var currentDir = Directory.GetCurrentDirectory();
AYANOVA_LOG_PATH = Path.Combine(currentDir, "logs");
}
else
{
AYANOVA_LOG_PATH = Path.Combine(AYANOVA_LOG_PATH, "logs");
}
//(note, startup.cs ensures these folders exist via FileUtil because we need IHostingEnvironment)
//UserFiles
AYANOVA_FOLDER_USER_FILES = config.GetValue<string>("AYANOVA_FOLDER_USER_FILES");
//BackupFiles
AYANOVA_FOLDER_BACKUP_FILES = config.GetValue<string>("AYANOVA_FOLDER_BACKUP_FILES");
#endregion server BASICS
#region METRICS
//InfluxDB
bTemp = config.GetValue<bool?>("AYANOVA_METRICS_USE_INFLUXDB");
AYANOVA_METRICS_USE_INFLUXDB = (null == bTemp) ? false : (bool)bTemp;
AYANOVA_METRICS_INFLUXDB_BASEURL = config.GetValue<string>("AYANOVA_METRICS_INFLUXDB_BASEURL");
AYANOVA_METRICS_INFLUXDB_BASEURL = string.IsNullOrWhiteSpace(AYANOVA_METRICS_INFLUXDB_BASEURL) ? "http://127.0.0.1:8086" : AYANOVA_METRICS_INFLUXDB_BASEURL;
AYANOVA_METRICS_INFLUXDB_DBNAME = config.GetValue<string>("AYANOVA_METRICS_INFLUXDB_DBNAME");
AYANOVA_METRICS_INFLUXDB_DBNAME = string.IsNullOrWhiteSpace(AYANOVA_METRICS_INFLUXDB_DBNAME) ? "AyaNova" : AYANOVA_METRICS_INFLUXDB_DBNAME;
AYANOVA_METRICS_INFLUXDB_CONSISTENCY = config.GetValue<string>("AYANOVA_METRICS_INFLUXDB_CONSISTENCY");
//No default value, if it's null or empty or whitespace then it won't be set
AYANOVA_METRICS_INFLUXDB_USERNAME = config.GetValue<string>("AYANOVA_METRICS_INFLUXDB_USERNAME");
AYANOVA_METRICS_INFLUXDB_USERNAME = string.IsNullOrWhiteSpace(AYANOVA_METRICS_INFLUXDB_USERNAME) ? "root" : AYANOVA_METRICS_INFLUXDB_USERNAME;
AYANOVA_METRICS_INFLUXDB_PASSWORD = config.GetValue<string>("AYANOVA_METRICS_INFLUXDB_PASSWORD");
AYANOVA_METRICS_INFLUXDB_PASSWORD = string.IsNullOrWhiteSpace(AYANOVA_METRICS_INFLUXDB_PASSWORD) ? "root" : AYANOVA_METRICS_INFLUXDB_PASSWORD;
AYANOVA_METRICS_INFLUXDB_RETENSION_POLICY = config.GetValue<string>("AYANOVA_METRICS_INFLUXDB_RETENSION_POLICY");
//No default value, if it's null or empty or whitespace then it won't be set
bTemp = config.GetValue<bool?>("AYANOVA_METRICS_INFLUXDB_CREATE_DATABASE_IF_NOT_EXISTS");
AYANOVA_METRICS_INFLUXDB_CREATE_DATABASE_IF_NOT_EXISTS = (null == bTemp) ? true : (bool)bTemp;
#endregion
}
//Fetch first url from list of urls (used by generator)
internal static string FirstOfAyaNovaUseUrls
{
get
{
if (string.IsNullOrWhiteSpace(AYANOVA_USE_URLS))
{ return null; }
if (!AYANOVA_USE_URLS.Contains(";"))
{
return AYANOVA_USE_URLS.Replace("*", "localhost");
}
var s = AYANOVA_USE_URLS.Split(';');
return s[0].Replace("*", "localhost");
}
}
}//eoc
}//eons

View File

@@ -0,0 +1,58 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using AyaNova.Models;
namespace AyaNova.Util
{
/// <summary>
/// Shared service provider for static classes
/// </summary>
internal static class ServiceProviderProvider
{
private static IServiceProvider _provider;
//CALL IT LIKE THIS:
// ApiServerState apiServerState = (ApiServerState)ServiceProviderProvider.Provider.GetService(typeof(ApiServerState));
/*
or is it like this??
using (IServiceScope scope = provider.CreateScope())
{
AyContext ct = scope.ServiceProvider.GetRequiredService<AyContext>();
ApiServerState serverState = scope.ServiceProvider.GetRequiredService<ApiServerState>();
*/
internal static IServiceProvider Provider
{
get
{
return _provider;
}
set
{
_provider = value;
}
}
internal static IServiceScope Scope
{
get
{
return Provider.CreateScope();
}
}
internal static AyContext DBContext
{
get
{
return Scope.ServiceProvider.GetRequiredService<AyContext>();
}
}
}
}

View File

@@ -0,0 +1,96 @@
using System;
namespace AyaNova.Util
{
internal static class StringUtil
{
/// <summary>
/// Extract string between tokens
/// </summary>
/// <param name="s"></param>
/// <param name="openTag"></param>
/// <param name="closeTag"></param>
/// <returns></returns>
public static string Extract(string s, string openTag, string closeTag)
{
int startIndex = s.IndexOf(openTag);
if (startIndex == -1)
throw new System.IndexOutOfRangeException("ExtractString->Error: open tag not found");
startIndex += openTag.Length;
int endIndex = s.IndexOf(closeTag, startIndex);
if (endIndex == -1)
throw new System.IndexOutOfRangeException("ExtractString->Error: closing tag not found");
return s.Substring(startIndex, endIndex - startIndex);
}
/// <summary>
/// Trim a string if necessary
/// </summary>
/// <param name="s"></param>
/// <param name="maxLength"></param>
/// <returns></returns>
public static string MaxLength(string s, int maxLength)
{
if (s.Length > maxLength)
s = s.Substring(0, maxLength);
return s;
}
/// <summary>
/// mask the exact ip address by substituting the last position of the address with XXX
/// Works with v6 or v4 addresses as strings
/// </summary>
/// <param name="sIP"></param>
/// <returns></returns>
public static string MaskIPAddress(string sIP)
{
//My test station ip address!?
//"::ffff:127.0.0.1"
//weird dual format, new method that covers both v4 and v4 inside v6 format
if(sIP.Contains("."))
{
//new algorithm, replace anything after last period with an xxx
var ret=sIP.Substring(0,sIP.LastIndexOf("."))+".xxx";
return ret;
}
//8 groups IPV6 Address format
if (sIP.Contains(":"))
{
sIP=sIP.Replace("::",":0:");//rehydrate "compressed" addresses
var segs = sIP.Split(':');
if (segs.Length < 7)
return "UNRECOGNIZED V6 IP ADDRESS FORMAT";
else
return segs[0] + ":" + segs[1] + ":" + segs[2] + ":" + segs[3] + ":" + segs[4] + ":" + segs[5] + ":" + segs[6] + ":" + segs[7] + ":xxxx";
}
// //4 groups IPV4 Address format
// if (sIP.Contains("."))
// {
// //8 groups IPV6 Address format
// var segs = sIP.Split('.');
// if (segs.Length < 3)
// return "UNRECOGNIZED V4 IP ADDRESS FORMAT";
// else
// return segs[0] + "." + segs[1] + "." + segs[2] + ".xxx";
// }
return "UNRECOGNIZED IP ADDDRESS FORMAT";
}
}//eoc
}//eons