Compare commits
14 Commits
net10-port
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f394aa6ee3 | |||
| 4dad213752 | |||
| 03ff2a2626 | |||
| 712d0c891f | |||
| e28dc48bd0 | |||
| 5a72164b68 | |||
| 809f46fde1 | |||
| 6d6c74fd32 | |||
| cd3ebed68b | |||
| f7c1d7c856 | |||
| b49bcaecff | |||
| 8f87f385de | |||
| d00ec199eb | |||
| daebf202e9 |
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
devdocs/*.sh text eol=lf
|
||||
8
CLAUDE.md
Normal file
8
CLAUDE.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Sockeye — instructions for Claude Code
|
||||
|
||||
Sockeye is the internal licensing server for AyaNova. Its server began as a copy of AyaNova's
|
||||
(`..\raven`), with some names changed: `devdocs/NAME-MAP.md` lists them, and
|
||||
`devdocs/compare-with-raven.sh` compares the shared files with the names mapped. For a low-level
|
||||
server fix in either codebase, check whether the other needs it too, and record the answer.
|
||||
|
||||
The working rules in `..\raven\CLAUDE.md` apply here as well.
|
||||
81
devdocs/NAME-MAP.md
Normal file
81
devdocs/NAME-MAP.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# AyaNova ↔ Sockeye name map
|
||||
|
||||
Sockeye's server began as a copy of AyaNova's (`raven/server/AyaNova`). Some names were
|
||||
changed for Sockeye; most code is otherwise the same. Use this map to carry a fix from one
|
||||
codebase to the other. Mapped 2026-10-27, AyaNova `main` (8.3.0) against Sockeye `main` (8.1.x).
|
||||
|
||||
`compare-with-raven.sh` (beside this file) applies the map and compares the shared files, so
|
||||
only real code differences show:
|
||||
|
||||
```bash
|
||||
bash compare-with-raven.sh # every shared file: differing lines, AyaNova lines, path
|
||||
bash compare-with-raven.sh util/DbUtil.cs # the diff for one file (< AyaNova mapped, > Sockeye)
|
||||
```
|
||||
|
||||
## Renamed
|
||||
|
||||
| AyaNova | Sockeye | Where |
|
||||
|---|---|---|
|
||||
| namespace `AyaNova.*`, `AyaNova` in text | `Sockeye.*`, `Sockeye` | everywhere |
|
||||
| `AyaType` (enum) | `SockType` | `biz/AyaType.cs` → `biz/SockType.cs` |
|
||||
| `AyaTypeId` | `SockTypeId` | `biz/` |
|
||||
| `AyaEvent` (enum) | `SockEvent` | `biz/` |
|
||||
| `AyaDaysOfWeek` (enum) | `SockDaysOfWeek` | `biz/` |
|
||||
| `AyaNovaVersion` | `SockeyeVersion` | `util/` |
|
||||
| `AyaNovaConnectionString`, `AyaNovaServerURL`, `logRuleAyaNovaItems` | `SockeyeConnectionString`, `SockeyeServerURL`, `logRuleSockeyeItems` | `util/` |
|
||||
| `AyaTypeTranslated`, `ayaTypeColumnName` | `SockTypeTranslated`, `sockTypeColumnName` | DataList, PickList |
|
||||
| stored type property `AType` (JSON `aType`) | `SockType` (JSON `sockType`) | `models/`: Attachment, Event, SearchKey, OpsJob, Report, Review, IntegrationItem, DataListSelectedRequest… |
|
||||
| computed `public AyaType AyaType { get => AyaType.X; }` (JSON `ayaType`) | `public SockType SType { get => SockType.X; }` (JSON `sType`) | business models: Customer, User, Memo, Reminder, Review… |
|
||||
| locals and parameters `ayaType`, `atype`, `ayId`, `ayaEvent` | `sockType`, `sockId`, `sockEvent` (a few `stype`, `sock`) | everywhere |
|
||||
| database columns `atype`, `ayatype`, `ayid`, `ayevent` | `socktype`, `sockid`, `sockevent`, and `stype` on business tables | `util/AySchema.cs`, raw SQL in DataList/PickList/biz |
|
||||
| config keys `AYANOVA_*` | `SOCKEYE_*`, same suffix | `util/ServerBootConfig.cs`, `config.json`, environment |
|
||||
| client `src/api/ayatype.js`, `ayanova-version.js` | `src/api/socktype.js`, `sockeye-version.js` | `raven-client` ↔ `sockeye-client` |
|
||||
|
||||
The shared `AyaType`/`SockType` members have the same numbers (Customer 8, User 3, Report 57…),
|
||||
and `AyaEvent`/`SockEvent` are identical, so stored values mean the same thing in both.
|
||||
|
||||
## Not renamed (same in both)
|
||||
|
||||
`AyContext`, `AySchema`, `AyaPickListFieldDefinition`, `AyaPickList`, `AyaField`, `AyImportData`,
|
||||
`AyFileInfo`, `AyaNovaLicenseKey`; table names (`aevent`, `asearchkey`, `auser`…); routes
|
||||
(`api/v{version}/…`); the folder layout (`biz`, `ControllerHelpers`, `Controllers`, `DataList`,
|
||||
`generator`, `kpi`, `models`, `PickList`, `resource`, `util`).
|
||||
|
||||
## Only in one codebase
|
||||
|
||||
- **AyaNova only:** the service objects (work orders, quotes, PM, parts, inventory, units,
|
||||
contracts, projects, rates, CSR, loan units, purchase orders, task groups, tax codes), their
|
||||
KPIs, `License.cs` (licence check, client side), `Seeder.cs`, `ReportApiRelay.cs`,
|
||||
`CoreJobLicense`, `CoreJobVersionCheck`, `CoreJobPM*`, `SuperUserDefaultPasswordRoutes`,
|
||||
`CustomerContactRoutes`, and newer config keys (`AYANOVA_TRUSTED_PROXIES`,
|
||||
`AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE`, `AYANOVA_REMOVE_LICENSE_FROM_DB`,
|
||||
`AYANOVA_REPORT_RENDER_API_URL_OVERRIDE`). Also `AyaDayOfWeek` and `AyaUnitsOfTime` (PM).
|
||||
- **Sockeye only:** licensing and sales (`KeyFactory`, `RavenKeyFactory`, `LicenseBiz`,
|
||||
products, purchases, subscriptions, subscription servers and their states, trial licence
|
||||
requests, vendor notifications, `GZCase`), `SockBot*` jobs, `FetchController`,
|
||||
`RvfController`, `RvrController`, `AutoOrderProcessingUtil`.
|
||||
|
||||
## How close the shared files are (2026-10-27)
|
||||
|
||||
288 server files exist in both. After mapping, 156 are identical, about 55 differ by 1–10
|
||||
lines, and the rest differ more, mostly because one codebase has objects the other lacks.
|
||||
Low-level files that differ by more than the names, worth checking when a fix touches them:
|
||||
|
||||
| File | Differing lines | Main reason |
|
||||
|---|---|---|
|
||||
| `util/RunProgram.cs` | most | AyaNova has `RunNoShell` (backup without a shell, 8.3.0); Sockeye still has the old `Run` |
|
||||
| `generator/CoreJobBackup.cs` | ~50 | same: Sockeye still passes the password in the `pg_dump` URI through a shell |
|
||||
| `util/DbUtil.cs`, `util/AySchema.cs` | many | different tables; check `AdminConnectionString` and drop-and-recreate by hand |
|
||||
| `DataList/DataListSqlFilterCriteriaBuilder.cs` | ~155 | filter fixes went into each separately; compare before changing either |
|
||||
| `util/ServerBootConfig.cs`, `Program.cs`, `Startup.cs` | many | config keys and service wiring differ |
|
||||
| `util/FileUtil.cs`, `util/StringUtil.cs`, `util/DateUtil.cs`, `generator/CoreJobNotify.cs`, `generator/CoreJobSweeper.cs`, `util/Mailer.cs` | 15–90 | partly real drift; check with the script |
|
||||
|
||||
Rerun the script for current numbers; this table is a snapshot.
|
||||
|
||||
## Carrying a fix across
|
||||
|
||||
1. Make and test the fix in one codebase.
|
||||
2. Run `bash compare-with-raven.sh <file>` on each file touched: if the file matches apart from
|
||||
names, the same change applies with the names above.
|
||||
3. Record in both repos whether the fix applies (done, n/a, or deferred). In AyaNova that is
|
||||
`raven/devdocs/modernization/NET10-PORT.md`.
|
||||
38
devdocs/compare-with-raven.sh
Normal file
38
devdocs/compare-with-raven.sh
Normal file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
# Compare Sockeye server source with AyaNova's (raven) after mapping AyaNova's names to
|
||||
# Sockeye's (see NAME-MAP.md), so only real code differences show.
|
||||
#
|
||||
# bash compare-with-raven.sh one line per shared file: differing lines, raven lines, path
|
||||
# bash compare-with-raven.sh util/DbUtil.cs the diff for one file (raven mapped < > sockeye)
|
||||
#
|
||||
# Read-only. Expects the repos side by side under one folder (C:\data\code).
|
||||
|
||||
set -euo pipefail
|
||||
here="$(cd "$(dirname "$0")" && pwd)"
|
||||
R="$here/../../raven/server/AyaNova"
|
||||
S="$here/../server"
|
||||
|
||||
# AyaNova name -> Sockeye name; order matters (longer names first)
|
||||
norm() {
|
||||
sed -E \
|
||||
-e 's/AyaNovaVersion/SockeyeVersion/g' \
|
||||
-e 's/AyaNova/Sockeye/g; s/ayanova/sockeye/g; s/AYANOVA_/SOCKEYE_/g' \
|
||||
-e 's/public AyaType AyaType/public SockType SType/g' \
|
||||
-e 's/AyaTypeId/SockTypeId/g; s/AyaType/SockType/g; s/ayaType/sockType/g; s/ayatype/socktype/g' \
|
||||
-e 's/\bAType\b/SockType/g; s/\batype\b/socktype/g' \
|
||||
-e 's/AyaEvent/SockEvent/g; s/ayaEvent/sockEvent/g; s/ayevent/sockevent/g' \
|
||||
-e 's/AyaDaysOfWeek/SockDaysOfWeek/g' \
|
||||
-e 's/\bAyId\b/SockId/g; s/\bayId\b/sockId/g; s/\bayid\b/sockid/g' \
|
||||
"$1" | tr -d '\r'
|
||||
}
|
||||
|
||||
if [ $# -gt 0 ]; then
|
||||
diff <(norm "$R/$1") <(tr -d '\r' < "$S/$1") || true
|
||||
exit 0
|
||||
fi
|
||||
|
||||
(cd "$R" && find . -name '*.cs' -not -path './obj/*' -not -path './bin/*') | sort | while read -r f; do
|
||||
[ -f "$S/$f" ] || continue
|
||||
d=$(diff <(norm "$R/$f") <(tr -d '\r' < "$S/$f") | grep -c '^[<>]' || true)
|
||||
echo "$d $(wc -l < "$R/$f") ${f#./}"
|
||||
done
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Asp.Versioning;
|
||||
@@ -35,6 +36,10 @@ namespace Sockeye.Api.Controllers
|
||||
private readonly ApiServerState serverState;
|
||||
private const int JWT_LIFETIME_DAYS = 5;
|
||||
|
||||
//case 4710: wrong 2fa codes allowed per password entry, counted per user (in memory, a restart only resets the count)
|
||||
private const int MAX_TFA_FAILURES = 5;
|
||||
private static readonly ConcurrentDictionary<long, int> TfaFailures = new ConcurrentDictionary<long, int>();
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
@@ -74,8 +79,14 @@ namespace Sockeye.Api.Controllers
|
||||
|
||||
|
||||
|
||||
//case 4726: too many failed logins for this login name or from this address: refused at once, the password is not checked
|
||||
var address = HttpContext.Connection.RemoteIpAddress;
|
||||
if (LoginThrottle.IsRefused(creds.Login, address))
|
||||
return StatusCode(429, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED, null, "Too many failed login attempts, please try again later"));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(creds.Login) || string.IsNullOrWhiteSpace(creds.Password))
|
||||
{
|
||||
RecordLoginFailure(creds.Login, address);
|
||||
//Make a failed pw wait
|
||||
await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
||||
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
||||
@@ -91,7 +102,7 @@ namespace Sockeye.Api.Controllers
|
||||
string hashed = Hasher.hash(u.Salt, creds.Password);
|
||||
if (hashed == u.Password)
|
||||
{
|
||||
|
||||
LoginThrottle.RecordSuccess(creds.Login);
|
||||
|
||||
//TWO FACTOR ENABLED??
|
||||
//if 2fa enabled then need to validate it before sending token, so we're halfway there and need to send a 2fa prompt
|
||||
@@ -100,6 +111,7 @@ namespace Sockeye.Api.Controllers
|
||||
//Generate a temporary token to identify and verify this is the same user
|
||||
u.TempToken = Hasher.GenerateSalt().Replace("=", "").Replace("+", "");
|
||||
await ct.SaveChangesAsync();
|
||||
TfaFailures.TryRemove(u.Id, out _);
|
||||
var UOpt = await ct.UserOptions.AsNoTracking().FirstAsync(z => z.UserId == u.Id);
|
||||
|
||||
List<string> TranslationKeysToFetch = new List<string> { "AuthTwoFactor", "AuthEnterPin", "AuthVerifyCode", "Cancel", "AuthPinInvalid" };
|
||||
@@ -125,11 +137,20 @@ namespace Sockeye.Api.Controllers
|
||||
}
|
||||
|
||||
//No users matched, it's a failed login
|
||||
RecordLoginFailure(creds.Login, address);
|
||||
//Make a failed pw wait
|
||||
await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
||||
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
||||
}
|
||||
|
||||
//case 4726
|
||||
private void RecordLoginFailure(string login, System.Net.IPAddress address)
|
||||
{
|
||||
var reached = LoginThrottle.RecordFailure(login, address);
|
||||
if (reached != null)
|
||||
log.LogWarning($"Too many failed login attempts for {reached}: further attempts refused for up to 15 minutes");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -164,6 +185,13 @@ namespace Sockeye.Api.Controllers
|
||||
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
||||
}
|
||||
|
||||
//case 4710: the temp token is the only proof the password step was passed, so it must be present
|
||||
if (string.IsNullOrWhiteSpace(pin.TempToken))
|
||||
{
|
||||
await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
||||
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
||||
}
|
||||
|
||||
//Match to temp token that would have been set by initial credentialed login for 2fa User
|
||||
var user = await ct.User.Where(z => z.TempToken == pin.TempToken && z.Active == true && z.AllowLogin==true && z.TwoFactorEnabled == true).FirstOrDefaultAsync();
|
||||
|
||||
@@ -181,14 +209,22 @@ namespace Sockeye.Api.Controllers
|
||||
var tfa = new TwoFactorAuth("Sockeye");
|
||||
if (!tfa.VerifyCode(user.TotpSecret, pin.Pin.Replace(" ", "").Trim()))
|
||||
{
|
||||
//case 4710: after too many wrong codes the password must be entered again
|
||||
if (TfaFailures.AddOrUpdate(user.Id, 1, (k, n) => n + 1) >= MAX_TFA_FAILURES)
|
||||
{
|
||||
TfaFailures.TryRemove(user.Id, out _);
|
||||
user.TempToken = null;
|
||||
await ct.SaveChangesAsync();
|
||||
}
|
||||
await Task.Delay(Sockeye.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
||||
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
||||
}
|
||||
|
||||
//User is valid and authenticated
|
||||
//clear temp token
|
||||
user.TempToken = string.Empty;
|
||||
//clear temp token (case 4710: null, not empty, so no value is left to match)
|
||||
user.TempToken = null;
|
||||
await ct.SaveChangesAsync();
|
||||
TfaFailures.TryRemove(user.Id, out _);
|
||||
|
||||
return await ReturnUserCredsOnSuccessfulAuthentication(user);
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ namespace Sockeye.Api.Controllers
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
// GET: fetch/somecode/bob@bob.com
|
||||
[HttpGet("{code}/{email}")]
|
||||
[Microsoft.AspNetCore.RateLimiting.EnableRateLimiting(Startup.RATE_LIMIT_LICENSE_FETCH)]//case 4681
|
||||
public async Task<IActionResult> Get([FromRoute] string code, [FromRoute] string email)
|
||||
{
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ namespace Sockeye.Api.Controllers
|
||||
//### RAVEN FETCH LICENSE REQUEST HERE ####
|
||||
//This route is used by Raven to check if there is an unfetched license key and return it
|
||||
[HttpPost]
|
||||
[Microsoft.AspNetCore.RateLimiting.EnableRateLimiting(Startup.RATE_LIMIT_LICENSE_FETCH)]//case 4681
|
||||
public async Task<IActionResult> PostLicenseFetchRequest([FromBody] dtoFetchRequest fetchRequest)
|
||||
{
|
||||
if (!serverState.IsOpen)
|
||||
|
||||
@@ -60,6 +60,7 @@ namespace Sockeye.Api.Controllers
|
||||
//REQUEST TRIAL ROUTE CALLED FROM RAVEN
|
||||
//#########################################################
|
||||
[HttpPost]
|
||||
[Microsoft.AspNetCore.RateLimiting.EnableRateLimiting(Startup.RATE_LIMIT_TRIAL_REQUESTS)]//case 4681
|
||||
public async Task<IActionResult> Post([FromBody] dtoRequest r)
|
||||
{
|
||||
if (!serverState.IsOpen)
|
||||
@@ -70,6 +71,12 @@ namespace Sockeye.Api.Controllers
|
||||
return BadRequest(ModelState);
|
||||
}
|
||||
|
||||
//case 4681: a verification email is sent to this address, so it must at least be a valid address
|
||||
if (string.IsNullOrWhiteSpace(r.Email) || !System.Net.Mail.MailAddress.TryCreate(r.Email.Trim(), out var parsedEmail) || parsedEmail.Address != r.Email.Trim())
|
||||
{
|
||||
return BadRequest("E1000 - Email address invalid");
|
||||
}
|
||||
|
||||
// if (r.DbId == Guid.Empty)
|
||||
if (string.IsNullOrWhiteSpace(r.DbId))
|
||||
{
|
||||
@@ -114,6 +121,7 @@ namespace Sockeye.Api.Controllers
|
||||
|
||||
//### CUSTOMER ROUTE CALLED FROM CUSTOMER BROWSER / EMAIL ####
|
||||
[HttpGet("verify/{code}")]
|
||||
[Microsoft.AspNetCore.RateLimiting.EnableRateLimiting(Startup.RATE_LIMIT_LICENSE_FETCH)]//case 4681
|
||||
public async Task<IActionResult> GetVerify([FromRoute] string code)
|
||||
{
|
||||
if (!serverState.IsOpen)
|
||||
|
||||
@@ -57,6 +57,7 @@ namespace Sockeye.Api.Controllers
|
||||
//REQUEST ROUTE CALLED FROM CONTACT FORMS APP
|
||||
//#########################################################
|
||||
[HttpPost]
|
||||
[Microsoft.AspNetCore.RateLimiting.EnableRateLimiting(Startup.RATE_LIMIT_TRIAL_REQUESTS)]//case 4681
|
||||
public async Task<IActionResult> Post([FromBody] dtoSubscriptionServerTrialRequest r)
|
||||
{
|
||||
if (!serverState.IsOpen)
|
||||
@@ -109,6 +110,9 @@ namespace Sockeye.Api.Controllers
|
||||
case "NY":
|
||||
newObject.TimeZone = "America/New_York";
|
||||
break;
|
||||
case "ATL":
|
||||
newObject.TimeZone = "America/New_York";//this is the correct time zone for Atlanta officially
|
||||
break;
|
||||
case "TOR":
|
||||
newObject.TimeZone = "America/Toronto";
|
||||
break;
|
||||
|
||||
@@ -28,6 +28,10 @@ namespace Sockeye
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
//case 4681: rate limit policy names used on the anonymous trial and licence routes
|
||||
internal const string RATE_LIMIT_TRIAL_REQUESTS = "trial-requests";
|
||||
internal const string RATE_LIMIT_LICENSE_FETCH = "license-fetch";
|
||||
|
||||
/////////////////////////////////////////////////////////////
|
||||
//
|
||||
public Startup(Microsoft.AspNetCore.Hosting.IWebHostEnvironment hostingEnvironment)
|
||||
@@ -174,6 +178,19 @@ namespace Sockeye
|
||||
);
|
||||
});
|
||||
|
||||
//case 4681: per client address limits on the anonymous trial request and licence fetch routes
|
||||
_newLog.LogDebug("Rate limiting");
|
||||
services.AddRateLimiter(options =>
|
||||
{
|
||||
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
|
||||
options.AddPolicy(RATE_LIMIT_TRIAL_REQUESTS, context => System.Threading.RateLimiting.RateLimitPartition.GetFixedWindowLimiter(
|
||||
context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
|
||||
_ => new System.Threading.RateLimiting.FixedWindowRateLimiterOptions { PermitLimit = 5, Window = TimeSpan.FromHours(1), QueueLimit = 0 }));
|
||||
options.AddPolicy(RATE_LIMIT_LICENSE_FETCH, context => System.Threading.RateLimiting.RateLimitPartition.GetFixedWindowLimiter(
|
||||
context.Connection.RemoteIpAddress?.ToString() ?? "unknown",
|
||||
_ => new System.Threading.RateLimiting.FixedWindowRateLimiterOptions { PermitLimit = 30, Window = TimeSpan.FromMinutes(10), QueueLimit = 0 }));
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -377,6 +394,9 @@ namespace Sockeye
|
||||
_newLog.LogDebug("Routing pipeline");
|
||||
app.UseRouting();//this wasn't here for 2.2 but added for 3.0, needs to come before the stuff after
|
||||
|
||||
//case 4681: after routing so the per-route policies apply (and after forwarded headers so the client address is the real one)
|
||||
app.UseRateLimiter();
|
||||
|
||||
_newLog.LogDebug("CORS pipeline");
|
||||
app.UseCors("CorsPolicy");
|
||||
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"IncludeScopes": false,
|
||||
"Debug": {
|
||||
"LogLevel": {
|
||||
"Default": "Trace"
|
||||
}
|
||||
},
|
||||
"Console": {
|
||||
"LogLevel": {
|
||||
"Default": "Trace"
|
||||
}
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,9 +535,6 @@ http://www.ayanova.com
|
||||
Answers to frequently asked questions:
|
||||
http://www.ayanova.com/faqanswers.htm
|
||||
|
||||
Technical support forum:
|
||||
http://forum.ayanova.com
|
||||
|
||||
Latest AyaNova version update information:
|
||||
Downloads and help documentation:
|
||||
http://www.ayanova.com/download.htm
|
||||
@@ -617,9 +614,6 @@ http://www.ayanova.com
|
||||
Answers to frequently asked questions:
|
||||
http://www.ayanova.com/faqanswers.htm
|
||||
|
||||
Technical support forum:
|
||||
http://forum.ayanova.com
|
||||
|
||||
Latest AyaNova version update information:
|
||||
Downloads and help documentation:
|
||||
http://www.ayanova.com/download.htm
|
||||
|
||||
@@ -107,9 +107,6 @@ http://www.ayanova.com
|
||||
Answers to frequently asked questions:
|
||||
http://www.ayanova.com/faqanswers.htm
|
||||
|
||||
Technical support forum:
|
||||
http://forum.ayanova.com
|
||||
|
||||
Latest AyaNova version update information:
|
||||
Downloads and help documentation:
|
||||
http://www.ayanova.com/download.htm
|
||||
@@ -174,9 +171,6 @@ http://www.ayanova.com
|
||||
Answers to frequently asked questions:
|
||||
http://www.ayanova.com/faqanswers.htm
|
||||
|
||||
Technical support forum:
|
||||
http://forum.ayanova.com
|
||||
|
||||
Latest AyaNova version update information:
|
||||
Downloads and help documentation:
|
||||
http://www.ayanova.com/download.htm
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<GenerateFullPaths>true</GenerateFullPaths>
|
||||
<Version>8.1.0</Version>
|
||||
<FileVersion>8.1.0.0</FileVersion>
|
||||
<Version>8.1.1</Version>
|
||||
<FileVersion>8.1.1.0</FileVersion>
|
||||
<ApplicationIcon>sockeye.ico</ApplicationIcon>
|
||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||
<noWarn>1591</noWarn>
|
||||
@@ -24,6 +24,7 @@
|
||||
<PackageReference Include="Enums.NET" Version="5.0.0" />
|
||||
<PackageReference Include="jose-jwt" Version="5.3.0" />
|
||||
<PackageReference Include="MailKit" Version="4.18.0" />
|
||||
<PackageReference Include="System.Security.Cryptography.Pkcs" Version="10.0.13" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.12" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.12" />
|
||||
<PackageReference Include="Asp.Versioning.Mvc" Version="10.2.1" />
|
||||
|
||||
78
server/util/LoginThrottle.cs
Normal file
78
server/util/LoginThrottle.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net;
|
||||
|
||||
namespace Sockeye.Util
|
||||
{
|
||||
/// <summary>
|
||||
/// case 4726: failed logins counted per login name and per client address (in memory, a restart resets the counts).
|
||||
/// Too many failures within the window and further attempts are refused until the window ends.
|
||||
/// Login names that don't exist are counted the same way, so the refusal says nothing about which logins exist.
|
||||
/// Requests from this machine (loopback) are not counted per address: behind nginx the real client address is seen,
|
||||
/// so loopback is only the machine itself
|
||||
/// </summary>
|
||||
internal static class LoginThrottle
|
||||
{
|
||||
internal const int MAX_LOGIN_FAILURES = 10;
|
||||
internal const int MAX_ADDRESS_FAILURES = 50;
|
||||
private static readonly TimeSpan WINDOW = TimeSpan.FromMinutes(15);
|
||||
private const int PRUNE_ABOVE = 10000;
|
||||
|
||||
private record Window(DateTime Start, int Count);
|
||||
private static readonly ConcurrentDictionary<string, Window> Failures = new ConcurrentDictionary<string, Window>();
|
||||
|
||||
private static string LoginKey(string login) => "L:" + (login ?? string.Empty).Trim().ToLowerInvariant();
|
||||
private static string AddressKey(IPAddress address) => address == null || IPAddress.IsLoopback(address) ? null : "A:" + address.ToString();
|
||||
|
||||
/// <summary>
|
||||
/// True if this login or this address has reached its limit in the current window
|
||||
/// </summary>
|
||||
internal static bool IsRefused(string login, IPAddress address)
|
||||
{
|
||||
return Reached(LoginKey(login), MAX_LOGIN_FAILURES) || Reached(AddressKey(address), MAX_ADDRESS_FAILURES);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Counts a failed login; returns a description when this failure reached a limit (for the log), otherwise null
|
||||
/// </summary>
|
||||
internal static string RecordFailure(string login, IPAddress address)
|
||||
{
|
||||
if (Failures.Count > PRUNE_ABOVE)
|
||||
Prune();
|
||||
string reached = null;
|
||||
if (Count(LoginKey(login)) == MAX_LOGIN_FAILURES)
|
||||
reached = $"login \"{login}\"";
|
||||
var addressKey = AddressKey(address);
|
||||
if (addressKey != null && Count(addressKey) == MAX_ADDRESS_FAILURES)
|
||||
reached = (reached == null ? "" : reached + " and ") + $"address {address}";
|
||||
return reached;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A correct password clears the count for that login
|
||||
/// </summary>
|
||||
internal static void RecordSuccess(string login)
|
||||
{
|
||||
Failures.TryRemove(LoginKey(login), out _);
|
||||
}
|
||||
|
||||
private static bool Reached(string key, int max)
|
||||
{
|
||||
return key != null && Failures.TryGetValue(key, out var w) && DateTime.UtcNow - w.Start < WINDOW && w.Count >= max;
|
||||
}
|
||||
|
||||
private static int Count(string key)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
return Failures.AddOrUpdate(key, new Window(now, 1), (_, w) => now - w.Start < WINDOW ? w with { Count = w.Count + 1 } : new Window(now, 1)).Count;
|
||||
}
|
||||
|
||||
private static void Prune()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
foreach (var kv in Failures)
|
||||
if (now - kv.Value.Start >= WINDOW)
|
||||
Failures.TryRemove(kv.Key, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,22 +51,36 @@ namespace Sockeye.Util
|
||||
internal static void SetNextBackup()
|
||||
{
|
||||
DateTime utcNow = DateTime.UtcNow;
|
||||
//case 4682: the backup time is a time on the server's clock; days are the server's local days
|
||||
DateTime localToday = TimeZoneInfo.ConvertTimeFromUtc(utcNow, TimeZoneInfo.Local).Date;
|
||||
//Has last backup run more than 24 hours ago?
|
||||
if (NextBackup < utcNow.AddHours(-24))
|
||||
{
|
||||
//set it to today then
|
||||
NextBackup = new DateTime(utcNow.Year, utcNow.Month, utcNow.Day, Backup.BackupTime.Hour, Backup.BackupTime.Minute, 0, DateTimeKind.Utc);
|
||||
NextBackup = BackupTimeOnLocalDay(localToday);
|
||||
//Make sure next backup is in the future
|
||||
if (NextBackup < utcNow)
|
||||
NextBackup = NextBackup.AddDays(1);
|
||||
NextBackup = BackupTimeOnLocalDay(localToday.AddDays(1));
|
||||
}
|
||||
else
|
||||
{
|
||||
//less than 24 hours, set it to tomorrow
|
||||
NextBackup = new DateTime(utcNow.Year, utcNow.Month, utcNow.Day, Backup.BackupTime.Hour, Backup.BackupTime.Minute, 0, DateTimeKind.Utc);
|
||||
NextBackup = NextBackup.AddDays(1);
|
||||
NextBackup = BackupTimeOnLocalDay(localToday.AddDays(1));
|
||||
}
|
||||
}
|
||||
|
||||
//case 4682: the backup time is stored as the UTC moment it meant on the day it was set, and was run at that UTC hour
|
||||
//every day, so it moved an hour on the server's clock with each daylight saving change. The server's clock time is
|
||||
//recovered with that day's offset and the backup is run at that clock time on the given local day (UTC returned)
|
||||
private static DateTime BackupTimeOnLocalDay(DateTime localDay)
|
||||
{
|
||||
var chosen = TimeZoneInfo.ConvertTimeFromUtc(DateTime.SpecifyKind(Backup.BackupTime, DateTimeKind.Utc), TimeZoneInfo.Local);
|
||||
var local = DateTime.SpecifyKind(localDay.Date + new TimeSpan(chosen.Hour, chosen.Minute, 0), DateTimeKind.Unspecified);
|
||||
//a time skipped when the clocks go forward (e.g. 2:30 AM that night) runs an hour later
|
||||
if (TimeZoneInfo.Local.IsInvalidTime(local))
|
||||
local = local.AddHours(1);
|
||||
return TimeZoneInfo.ConvertTimeToUtc(local, TimeZoneInfo.Local);
|
||||
}
|
||||
|
||||
}//eoc
|
||||
}//eons
|
||||
@@ -5,7 +5,7 @@ namespace Sockeye.Util
|
||||
/// </summary>
|
||||
internal static class SockeyeVersion
|
||||
{
|
||||
public const string VersionString = "8.0.17";
|
||||
public const string VersionString = "8.1.1";
|
||||
public const string FullNameAndVersion = "Sockeye server " + VersionString;
|
||||
public const string CurrentApiVersion="v8";
|
||||
}//eoc
|
||||
|
||||
@@ -178,11 +178,11 @@ namespace Sockeye.Util
|
||||
|
||||
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
var stringChars = new char[10];
|
||||
var random = new Random();
|
||||
|
||||
//case 4681: cryptographic random, same format as before
|
||||
for (int i = 0; i < stringChars.Length; i++)
|
||||
{
|
||||
stringChars[i] = chars[random.Next(chars.Length)];
|
||||
stringChars[i] = chars[System.Security.Cryptography.RandomNumberGenerator.GetInt32(chars.Length)];
|
||||
}
|
||||
|
||||
var finalString = new String(stringChars);
|
||||
|
||||
13
todo.txt
13
todo.txt
@@ -1,12 +1,17 @@
|
||||
TODO:
|
||||
|
||||
Chromium / chrome change is in the changelog of ayanova, no big mystery just some kind of default:
|
||||
AyaNova 8.2.0
|
||||
Released 2024-06-03
|
||||
Server: switched the report rendering system from Chromium to Chrome browser to align with the default browser now supported by the Puppeteer headless browser controller library that we use for report generation. There will be a longer than normal delay the first time a report is rendered after upgrading while AyaNova automatically downloads the new report rendering browser.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
- Subscriptions
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
todo: Two reports, one revenue report for list as filterd and one showing who subscribes to what and count of subscription items for each type etc
|
||||
- Need to make it reportable so the subscription biz object needs to be able to support reporting fully with renewal and date and price viz etc
|
||||
- Need to update the subscription item edit form to show renewal checkbox
|
||||
|
||||
Reference in New Issue
Block a user