This commit is contained in:
16
server/AyaNova/util/ApplicationLogging.cs
Normal file
16
server/AyaNova/util/ApplicationLogging.cs
Normal 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);
|
||||
|
||||
}
|
||||
}
|
||||
286
server/AyaNova/util/AySchema.cs
Normal file
286
server/AyaNova/util/AySchema.cs
Normal 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
|
||||
}
|
||||
29
server/AyaNova/util/AyaNovaVersion.cs
Normal file
29
server/AyaNova/util/AyaNovaVersion.cs
Normal 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
|
||||
65
server/AyaNova/util/CopyObject.cs
Normal file
65
server/AyaNova/util/CopyObject.cs
Normal 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
|
||||
69
server/AyaNova/util/DateUtil.cs
Normal file
69
server/AyaNova/util/DateUtil.cs
Normal 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
|
||||
480
server/AyaNova/util/DbUtil.cs
Normal file
480
server/AyaNova/util/DbUtil.cs
Normal 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
|
||||
36
server/AyaNova/util/EnumAttributeExtension.cs
Normal file
36
server/AyaNova/util/EnumAttributeExtension.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
32
server/AyaNova/util/ExceptionUtil.cs
Normal file
32
server/AyaNova/util/ExceptionUtil.cs
Normal 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
|
||||
25
server/AyaNova/util/FileHash.cs
Normal file
25
server/AyaNova/util/FileHash.cs
Normal 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
|
||||
470
server/AyaNova/util/FileUtil.cs
Normal file
470
server/AyaNova/util/FileUtil.cs
Normal 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
|
||||
53
server/AyaNova/util/Hasher.cs
Normal file
53
server/AyaNova/util/Hasher.cs
Normal 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
|
||||
31
server/AyaNova/util/IsLocalExtension.cs
Normal file
31
server/AyaNova/util/IsLocalExtension.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
645
server/AyaNova/util/License.cs
Normal file
645
server/AyaNova/util/License.cs
Normal 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
|
||||
167
server/AyaNova/util/MetricsRegistry.cs
Normal file
167
server/AyaNova/util/MetricsRegistry.cs
Normal 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"
|
||||
// };
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
53
server/AyaNova/util/RetryHelper.cs
Normal file
53
server/AyaNova/util/RetryHelper.cs
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
286
server/AyaNova/util/Seeder.cs
Normal file
286
server/AyaNova/util/Seeder.cs
Normal 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
|
||||
190
server/AyaNova/util/ServerBootConfig.cs
Normal file
190
server/AyaNova/util/ServerBootConfig.cs
Normal 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
|
||||
58
server/AyaNova/util/ServiceProviderProvider.cs
Normal file
58
server/AyaNova/util/ServiceProviderProvider.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
96
server/AyaNova/util/StringUtil.cs
Normal file
96
server/AyaNova/util/StringUtil.cs
Normal 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
|
||||
Reference in New Issue
Block a user