diff --git a/devdocs/todo.txt b/devdocs/todo.txt
index 85713b20..175ad522 100644
--- a/devdocs/todo.txt
+++ b/devdocs/todo.txt
@@ -75,6 +75,7 @@ TODO CLIENT STUFF
TODO SERVER STUFF
- Add license violation check - valid number of techs for license in multiple places
+ - do a big seed and see if it fails
- Trial license ROCKFISH up it to 1000 techs to cover huge seeding.
diff --git a/server/AyaNova/Startup.cs b/server/AyaNova/Startup.cs
index 5bdeb77a..2ad8611b 100644
--- a/server/AyaNova/Startup.cs
+++ b/server/AyaNova/Startup.cs
@@ -419,7 +419,7 @@ namespace AyaNova
if (TESTING_REFRESH_DB)
{
AyaNova.Core.License.Fetch(apiServerState, dbContext, _log);
- Util.Seeder.SeedDatabase(Util.Seeder.SeedLevel.SmallOneManShopTrialDataSet, -8);//#############################################################################################
+ Util.Seeder.SeedDatabase(Util.Seeder.SeedLevel.LargeCorporateMultiRegionalTrialDataSet, -8);//#############################################################################################
}
//TESTING
#endif
diff --git a/server/AyaNova/biz/JobsBiz.cs b/server/AyaNova/biz/JobsBiz.cs
index 27ee98d2..3ef28a37 100644
--- a/server/AyaNova/biz/JobsBiz.cs
+++ b/server/AyaNova/biz/JobsBiz.cs
@@ -282,7 +282,7 @@ namespace AyaNova.Biz
/// Process all jobs (stock jobs and those found in operations table)
///
///
- internal static async Task ProcessJobsAsync(AyContext ct)
+ internal static async Task ProcessJobsAsync(AyContext ct, AyaNova.Api.ControllerHelpers.ApiServerState serverState)
{
//Flush metrics report before anything else happens
log.LogTrace("Flushing metrics to reporters");
@@ -344,7 +344,18 @@ namespace AyaNova.Biz
//Health check / metrics
await CoreJobMetricsSnapshot.DoJobAsync(ct);
- //License check??
+ //License check
+ long CurrentActiveCount = UserBiz.ActiveCount;
+ long LicensedUserCount = AyaNova.Core.License.ActiveKey.ActiveNumber;
+ // log.LogInformation("JobsBiz::Checking license active count");
+ if (CurrentActiveCount > LicensedUserCount)
+ {
+ var msg = $"E1020 - Active count exceeded capacity";
+ serverState.SetSystemLock(msg);
+ log.LogCritical(msg);
+ return;
+ }
+
//Notifications
diff --git a/server/AyaNova/biz/UserBiz.cs b/server/AyaNova/biz/UserBiz.cs
index bdf462ae..87d03cca 100644
--- a/server/AyaNova/biz/UserBiz.cs
+++ b/server/AyaNova/biz/UserBiz.cs
@@ -30,9 +30,14 @@ namespace AyaNova.Biz
SeedOrImportRelaxedRulesMode = false;//default
}
- //todo:
- //then after that go into widget and anywhere else that there is this associated type code being called for event and search and implement everywhere,
- //then update seeder code to use it and get back on to the main critical path again in the todo
+ //This is where active tech license consumers are accounted for
+ internal static long ActiveCount
+ {
+ get
+ {
+ return ServiceProviderProvider.DBContext.User.Where(x => x.Active == true && (x.UserType == UserType.Schedulable || x.UserType == UserType.Subcontractor)).LongCount();
+ }
+ }
internal static UserBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext)
{
@@ -53,35 +58,34 @@ namespace AyaNova.Biz
inObj.Salt = Hasher.GenerateSalt();
inObj.Password = Hasher.hash(inObj.Salt, inObj.Password);
+ inObj.OwnerId = UserId;
+ inObj.Tags = TagUtil.NormalizeTags(inObj.Tags);
+ //Seeder sets user options in advance so no need to create them here in that case
+ if (inObj.UserOptions == null)
+ inObj.UserOptions = new UserOptions(UserId);
+
+
Validate(inObj, null);
if (HasErrors)
return null;
else
{
- //do stuff with User
- User outObj = inObj;
- outObj.OwnerId = UserId;
- outObj.Tags = TagUtil.NormalizeTags(outObj.Tags);
- //Seeder sets user options in advance so no need to create them here in that case
- if (outObj.UserOptions == null)
- outObj.UserOptions = new UserOptions(UserId);
- await ct.User.AddAsync(outObj);
+ await ct.User.AddAsync(inObj);
//save to get Id
await ct.SaveChangesAsync();
//Handle child and associated items
//Log event
- EventLogProcessor.LogEventToDatabase(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), ct);
+ EventLogProcessor.LogEventToDatabase(new Event(UserId, inObj.Id, BizType, AyaEvent.Created), ct);
- //SEARCH INDEXING
- //Search.ProcessNewObjectKeywords( UserLocaleId, outObj.Id, BizType, outObj.Name, outObj.EmployeeNumber, outObj.Notes, outObj.Name);
- var SearchParams = new Search.SearchIndexProcessObjectParameters(UserLocaleId, outObj.Id, BizType, outObj.Name);
- SearchParams.AddWord(outObj.Notes).AddWord(outObj.Name).AddWord(outObj.EmployeeNumber).AddWord(outObj.Tags);
+ //SEARCH INDEXING
+ var SearchParams = new Search.SearchIndexProcessObjectParameters(UserLocaleId, inObj.Id, BizType, inObj.Name);
+ SearchParams.AddWord(inObj.Notes).AddWord(inObj.Name).AddWord(inObj.EmployeeNumber).AddWord(inObj.Tags);
Search.ProcessNewObjectKeywords(SearchParams);
- return outObj;
+ return inObj;
}
}
@@ -93,36 +97,34 @@ namespace AyaNova.Biz
//This is a new user so it will have been posted with a password in plaintext which needs to be salted and hashed
inObj.Salt = Hasher.GenerateSalt();
inObj.Password = Hasher.hash(inObj.Salt, inObj.Password);
+ inObj.OwnerId = UserId;
+ inObj.Tags = TagUtil.NormalizeTags(inObj.Tags);
+ //Seeder sets user options in advance so no need to create them here in that case
+ if (inObj.UserOptions == null)
+ inObj.UserOptions = new UserOptions(UserId);
Validate(inObj, null);
if (HasErrors)
return null;
else
{
- //do stuff with User
- User outObj = inObj;
- outObj.OwnerId = UserId;
- outObj.Tags = TagUtil.NormalizeTags(outObj.Tags);
- //Seeder sets user options in advance so no need to create them here in that case
- if (outObj.UserOptions == null)
- outObj.UserOptions = new UserOptions(UserId);
- TempContext.User.Add(outObj);
+ TempContext.User.Add(inObj);
+
//save to get Id
TempContext.SaveChanges();
//Handle child and associated items
//Log event
- EventLogProcessor.LogEventToDatabase(new Event(UserId, outObj.Id, BizType, AyaEvent.Created), TempContext);
+ EventLogProcessor.LogEventToDatabase(new Event(UserId, inObj.Id, BizType, AyaEvent.Created), TempContext);
//SEARCH INDEXING
- // Search.ProcessNewObjectKeywords(UserLocaleId, outObj.Id, BizType, outObj.Name, outObj.EmployeeNumber, outObj.Notes, outObj.Name);
- var SearchParams = new Search.SearchIndexProcessObjectParameters(UserLocaleId, outObj.Id, BizType, outObj.Name);
- SearchParams.AddWord(outObj.Notes).AddWord(outObj.Name).AddWord(outObj.EmployeeNumber).AddWord(outObj.Tags);
+ var SearchParams = new Search.SearchIndexProcessObjectParameters(UserLocaleId, inObj.Id, BizType, inObj.Name);
+ SearchParams.AddWord(inObj.Notes).AddWord(inObj.Name).AddWord(inObj.EmployeeNumber).AddWord(inObj.Tags);
Search.ProcessNewObjectKeywords(SearchParams);
- return outObj;
+ return inObj;
}
}
@@ -229,51 +231,6 @@ namespace AyaNova.Biz
//get picklist (paged)
internal ApiPagedResponse GetPickList(IUrlHelper Url, string routeName, PagingOptions pagingOptions)
{
- // pagingOptions.Offset = pagingOptions.Offset ?? PagingOptions.DefaultOffset;
- // pagingOptions.Limit = pagingOptions.Limit ?? PagingOptions.DefaultLimit;
-
- // NameIdItem[] items;
- // int totalRecordCount = 0;
-
- // if (!string.IsNullOrWhiteSpace(q))
- // {
- // items = await ct.User
- // .AsNoTracking()
- // .Where(m => EF.Functions.ILike(m.Name, q))
- // .OrderBy(m => m.Name)
- // .Skip(pagingOptions.Offset.Value)
- // .Take(pagingOptions.Limit.Value)
- // .Select(m => new NameIdItem()
- // {
- // Id = m.Id,
- // Name = m.Name
- // }).ToArrayAsync();
-
- // totalRecordCount = await ct.User.Where(m => EF.Functions.ILike(m.Name, q)).CountAsync();
- // }
- // else
- // {
- // items = await ct.User
- // .AsNoTracking()
- // .OrderBy(m => m.Name)
- // .Skip(pagingOptions.Offset.Value)
- // .Take(pagingOptions.Limit.Value)
- // .Select(m => new NameIdItem()
- // {
- // Id = m.Id,
- // Name = m.Name
- // }).ToArrayAsync();
-
- // totalRecordCount = await ct.User.CountAsync();
- // }
-
-
-
- // var pageLinks = new PaginationLinkBuilder(Url, routeName, null, pagingOptions, totalRecordCount).PagingLinksObject();
-
- // ApiPagedResponse pr = new ApiPagedResponse(items, pageLinks);
- // return pr;
-
pagingOptions.Offset = pagingOptions.Offset ?? PagingOptions.DefaultOffset;
pagingOptions.Limit = pagingOptions.Limit ?? PagingOptions.DefaultLimit;
@@ -287,8 +244,6 @@ namespace AyaNova.Biz
}
-
-
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
@@ -301,24 +256,24 @@ namespace AyaNova.Biz
inObj.OwnerId = dbObj.OwnerId;
//Get a snapshot of the original db value object before changes
- User SnapshotObj = new User();
- CopyObject.Copy(dbObj, SnapshotObj);
+ User SnapshotOfOriginalDBObj = new User();
+ CopyObject.Copy(dbObj, SnapshotOfOriginalDBObj);
//Update the db object with the PUT object values
CopyObject.Copy(inObj, dbObj, "Id, Salt");
dbObj.Tags = TagUtil.NormalizeTags(dbObj.Tags);
//Is the user updating the password?
- if (!string.IsNullOrWhiteSpace(inObj.Password) && SnapshotObj.Password != inObj.Password)
+ if (!string.IsNullOrWhiteSpace(inObj.Password) && SnapshotOfOriginalDBObj.Password != inObj.Password)
{
//YES password is being updated:
- dbObj.Password = Hasher.hash(SnapshotObj.Salt, inObj.Password);
+ dbObj.Password = Hasher.hash(SnapshotOfOriginalDBObj.Salt, inObj.Password);
}
else
{
//No, use the snapshot password value
- dbObj.Password = SnapshotObj.Password;
- dbObj.Salt = SnapshotObj.Salt;
+ dbObj.Password = SnapshotOfOriginalDBObj.Password;
+ dbObj.Salt = SnapshotOfOriginalDBObj.Salt;
}
@@ -326,7 +281,7 @@ namespace AyaNova.Biz
//this will allow EF to check it out
ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = inObj.ConcurrencyToken;
- Validate(dbObj, SnapshotObj);
+ Validate(dbObj, SnapshotOfOriginalDBObj);
if (HasErrors)
return false;
@@ -349,22 +304,22 @@ namespace AyaNova.Biz
if (!ValidateJsonPatch.Validate(this, objectPatch)) return false;
//make a snapshot of the original for validation but update the original to preserve workflow
- User snapshotObj = new User();
- CopyObject.Copy(dbObj, snapshotObj);
+ User SnapshotOfOriginalDBObj = new User();
+ CopyObject.Copy(dbObj, SnapshotOfOriginalDBObj);
//Do the patching
objectPatch.ApplyTo(dbObj);
dbObj.Tags = TagUtil.NormalizeTags(dbObj.Tags);
//Is the user patching the password?
- if (!string.IsNullOrWhiteSpace(dbObj.Password) && dbObj.Password != snapshotObj.Password)
+ if (!string.IsNullOrWhiteSpace(dbObj.Password) && dbObj.Password != SnapshotOfOriginalDBObj.Password)
{
//YES password is being updated:
dbObj.Password = Hasher.hash(dbObj.Salt, dbObj.Password);
}
ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = concurrencyToken;
- Validate(dbObj, snapshotObj);
+ Validate(dbObj, SnapshotOfOriginalDBObj);
if (HasErrors)
return false;
@@ -424,11 +379,36 @@ namespace AyaNova.Biz
bool isNew = currentObj == null;
- if (isNew) //Yes, no currentObj
- {
+ //do we need to check the license situation?
+ if (proposedObj.IsTech && proposedObj.Active)
+ {
+ //Yes, it might be affected depending on things
+ long CurrentActiveCount = UserBiz.ActiveCount;
+ long LicensedUserCount = AyaNova.Core.License.ActiveKey.ActiveNumber;
+
+ if (isNew)
+ {
+ //This operation is about to consume one more license, check that we are not at the limit already
+ CheckActiveForValidation(CurrentActiveCount, LicensedUserCount);
+ }
+ else
+ {
+ //did anything that might affect licensing change?
+ if (!currentObj.IsTech || (!currentObj.Active))//currently not a tech or if it is it's not active
+ {
+ //going from non tech to tech and active
+ //Yes, this is about to consume one more license, check that we are not at the limit already
+ CheckActiveForValidation(CurrentActiveCount, LicensedUserCount);
+ }
+ }
}
+
+ // TODO: check user count if not new to see if affected that way
+ //also check user count in general to see if it's exceeded
+ //And maybe check it in login as well as a good central spot or wherever makes sense
+
//OwnerId required
if (!isNew)
{
@@ -543,6 +523,14 @@ namespace AyaNova.Biz
return;
}
+ private void CheckActiveForValidation(long CurrentActiveCount, long LicensedUserCount)
+ {
+ if (CurrentActiveCount >= LicensedUserCount)
+ {
+ AddError(ValidationErrorType.InvalidOperation, null, "LT:ErrorSecurityUserCapacity");
+ }
+ }
+
//Can delete?
private void ValidateCanDelete(User inObj)
@@ -560,7 +548,7 @@ namespace AyaNova.Biz
//There's only one rule - have they done anything eventlog worthy yet?
if (ct.Event.Select(m => m).Where(m => m.OwnerId == inObj.Id).Count() > 0)
{
- AddError(ValidationErrorType.InvalidOperation, "user", "[E_ACTIVE_NOT_DELETABLE] This user shows activity in the database and can not be deleted. Set inactive instead.");
+ AddError(ValidationErrorType.InvalidOperation, "user", "LT:ErrorDBForeignKeyViolation");
return;
}
diff --git a/server/AyaNova/biz/WidgetBiz.cs b/server/AyaNova/biz/WidgetBiz.cs
index d1c9e4a2..f71c168d 100644
--- a/server/AyaNova/biz/WidgetBiz.cs
+++ b/server/AyaNova/biz/WidgetBiz.cs
@@ -332,6 +332,8 @@ namespace AyaNova.Biz
//run validation and biz rules
if (isNew)
{
+ //WARNING: this is not really the "current" object, it's been modified already by caller
+
// //NEW widgets must be active
// if (inObj.Active == null || ((bool)inObj.Active) == false)
// {
diff --git a/server/AyaNova/generator/Generate.cs b/server/AyaNova/generator/Generate.cs
index 33c53a8c..9baaf9fc 100644
--- a/server/AyaNova/generator/Generate.cs
+++ b/server/AyaNova/generator/Generate.cs
@@ -86,7 +86,7 @@ namespace AyaNova.Generator
//=================================================================
try
{
- await JobsBiz.ProcessJobsAsync(ct);
+ await JobsBiz.ProcessJobsAsync(ct, serverState);
}
catch (Exception ex)
{
diff --git a/server/AyaNova/models/User.cs b/server/AyaNova/models/User.cs
index ed0fe75a..9f818222 100644
--- a/server/AyaNova/models/User.cs
+++ b/server/AyaNova/models/User.cs
@@ -46,7 +46,15 @@ namespace AyaNova.Models
public User()
{
- Tags = new List();
+ Tags = new List();
+ }
+
+ public bool IsTech
+ {
+ get
+ {
+ return this.UserType == UserType.Subcontractor || this.UserType == UserType.Schedulable;
+ }
}
}
diff --git a/server/AyaNova/util/License.cs b/server/AyaNova/util/License.cs
index 4eb8ea0a..3747571f 100644
--- a/server/AyaNova/util/License.cs
+++ b/server/AyaNova/util/License.cs
@@ -104,6 +104,13 @@ namespace AyaNova.Core
return null;
}
+ public long ActiveNumber
+ {
+ get
+ {
+ return GetLicenseFeature(SERVICE_TECHS_FEATURE_NAME).Count;
+ }
+ }
///
/// Check for the existance of license feature
@@ -446,7 +453,7 @@ namespace AyaNova.Core
if (ldb.Key == "none")
{
- var msg = "License key not found in database, running in unlicensed mode";
+ var msg = "E1020 - License key not found in database, running in unlicensed mode";
apiServerState.SetSystemLock(msg);
log.LogWarning(msg);
return;
@@ -456,9 +463,9 @@ namespace AyaNova.Core
AyaNovaLicenseKey k = Parse(ldb.Key, log);
if (k == null)
{
- var msg = "Error: License key in database is not valid, running in unlicensed mode";
+ var msg = "E1020 - License key in database is not valid, running in unlicensed mode";
apiServerState.SetSystemLock(msg);
- log.LogError(msg);
+ log.LogCritical(msg);
return;
}
@@ -466,9 +473,18 @@ namespace AyaNova.Core
if (_ActiveLicense.LicenseExpired)
{
- var msg = $"License key expired {DateUtil.ServerDateTimeString(_ActiveLicense.LicenseExpiration)}";
+ var msg = $"E1020 - License key expired {DateUtil.ServerDateTimeString(_ActiveLicense.LicenseExpiration)}";
apiServerState.SetSystemLock(msg);
- log.LogWarning(msg);
+ log.LogCritical(msg);
+ return;
+ }
+
+ //Has someone been trying funny business with the active techs in the db?
+ if (AyaNova.Biz.UserBiz.ActiveCount > _ActiveLicense.ActiveNumber)
+ {
+ var msg = $"E1020 - Active count exceeded capacity";
+ apiServerState.SetSystemLock(msg);
+ log.LogCritical(msg);
return;
}
@@ -484,7 +500,8 @@ namespace AyaNova.Core
catch (Exception ex)
{
var msg = "E1020 - Error initializing license key";
- log.LogError(ex, msg);
+ log.LogCritical(ex, msg);
+ apiServerState.SetSystemLock(msg);
throw new ApplicationException(msg, ex);
}
}
@@ -503,7 +520,7 @@ namespace AyaNova.Core
if (ParsedNewKey == null)
{
- throw new ApplicationException("License.Install -> key could not be parsed");
+ throw new ApplicationException("E1020 - License.Install -> key could not be parsed");
}
//Can't install a trial into a non-empty db
@@ -512,6 +529,12 @@ namespace AyaNova.Core
throw new ApplicationException("E1020 - Can't install a trial key into a non empty AyaNova database. Erase the database first.");
}
+ //TODO: TECHCOUNT - new license causes exceeding count?
+ if (AyaNova.Biz.UserBiz.ActiveCount > ParsedNewKey.GetLicenseFeature(SERVICE_TECHS_FEATURE_NAME).Count)
+ {
+ throw new ApplicationException("E1020 - Can't install key, too many active techs and / or subcontractors in database. Deactivate enough to install key.");
+ }
+
//Update current license
CurrentInDbKeyRecord.Key = RawTextNewKey;
//LOOKAT: reason, resultcode etc
@@ -547,7 +570,7 @@ namespace AyaNova.Core
if (string.IsNullOrWhiteSpace(k))
{
- throw new ApplicationException("License.Parse -> License key is empty and can't be validated");
+ throw new ApplicationException("E1020 - License.Parse -> License key is empty and can't be validated");
}
try
@@ -557,7 +580,7 @@ namespace AyaNova.Core
!k.Contains("[SIGNATURE") ||
!k.Contains("SIGNATURE]"))
{
- throw new ApplicationException("License.Parse -> License key is missing required delimiters");
+ throw new ApplicationException("E1020 - License.Parse -> License key is missing required delimiters");
}
@@ -588,7 +611,7 @@ EQIDAQAB
signer.BlockUpdate(msgBytes, 0, msgBytes.Length);
if (!signer.VerifySignature(expectedSig))
{
- throw new ApplicationException("License.Parse -> License key failed integrity check and is not valid");
+ throw new ApplicationException("E1020 - License.Parse -> License key failed integrity check and is not valid");
}
#endregion check signature
@@ -598,7 +621,7 @@ EQIDAQAB
key.LicenseFormat = (string)token.SelectToken("Key.LicenseFormat");
if (key.LicenseFormat != "2018")
- throw new ApplicationException($"License.Parse -> License key format {key.LicenseFormat} not recognized");
+ throw new ApplicationException($"E1020 - 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");