Compare commits
181 Commits
net8-basel
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 737951548c | |||
| 1393907bc7 | |||
| 541d56fc05 | |||
| 7d440530b3 | |||
| 413af08918 | |||
| 36fe2eaf53 | |||
| 3c3e431306 | |||
| d4bec5b381 | |||
| 910e022016 | |||
| 393a60c57d | |||
| c52273f284 | |||
| be500d2279 | |||
| d34adeb321 | |||
| e8b3486a9e | |||
| 6d3b169f23 | |||
| 91e8f54074 | |||
| ea0a3615ee | |||
| 3b80f019ad | |||
| 2968ae7dc7 | |||
| bd8896fdb6 | |||
| ad5b797854 | |||
| 903f14ba1d | |||
| 3968302f75 | |||
| 557cca3472 | |||
| 05cf4df632 | |||
| 15094275af | |||
| d37688494a | |||
| c19a3b047f | |||
| 1dd6783aff | |||
| be29f49ec1 | |||
| c6c9e6c5d3 | |||
| ad5da2ffa5 | |||
| b9035b93b0 | |||
| 72a64159c4 | |||
| bf1e447eee | |||
| 2bcd51c21d | |||
| 4ca6c9b2e3 | |||
| 5afd804586 | |||
| aca09a8c85 | |||
| 8a2b5d4829 | |||
| 356c73d854 | |||
| 176a90d3f3 | |||
| a715c3913c | |||
| a9be1ba9e7 | |||
| a9d5cab7e5 | |||
| e5b1f1dc14 | |||
| c55b2fc723 | |||
| 64edeea874 | |||
| 1c97678498 | |||
| b3b7fa6a9d | |||
| 4aa9ee54e6 | |||
| 8f7396a021 | |||
| ba84ebf7c9 | |||
| 450acc5ef7 | |||
| 7959ad9cec | |||
| 27b56db6b0 | |||
| ed641c4985 | |||
| a53a4b6ef7 | |||
| b116034dc9 | |||
| a807693187 | |||
| 2f616cba7a | |||
| a6447ecb37 | |||
| ce5fb9e11c | |||
| e9b79242fd | |||
| c04de33af2 | |||
| 344106ae27 | |||
| b99cbf126b | |||
| d084a461ea | |||
| 07d2b07fab | |||
| 1ccbd7745c | |||
| cce07f3e6b | |||
| b0e3933a9f | |||
| 973b64a682 | |||
| fa7affc91c | |||
| 8dc0ffd481 | |||
| 289058bad2 | |||
| eb1deff2ba | |||
| d8be701bcd | |||
| 80f81aea0f | |||
| 4141217c04 | |||
| 94a6f3b653 | |||
| f3ba5156c7 | |||
| 52d66bcf10 | |||
| 7204b9ff6d | |||
| 9fe52514b9 | |||
| 823bd5029a | |||
| d254affc8b | |||
| ee59efff48 | |||
| 6edb57c749 | |||
| 87fdc12dce | |||
| 351eb2d851 | |||
| 443d382394 | |||
| 692fececc7 | |||
| 1e32d59b8f | |||
| d75c3eecea | |||
| bd4494abac | |||
| 2b1adaa496 | |||
| 7b40caec8b | |||
| 445f9b0d0a | |||
| b1944b7ca3 | |||
| e58ab2c100 | |||
| 705a54f9d8 | |||
| a843bd3132 | |||
| 5bf1f2700a | |||
| 141bdeb89c | |||
| 27e187bac6 | |||
| 229fe208a7 | |||
| 67205ccf41 | |||
| ba60825167 | |||
| 8a54c020c5 | |||
| d6a7e024f0 | |||
| 234d96aaa6 | |||
| 40eb36080b | |||
| fc3342df48 | |||
| 52763366c2 | |||
| f1f01b47f1 | |||
| f90dfe98c7 | |||
| 8642d6e627 | |||
| 52c85f3aaa | |||
| 21c55e8d01 | |||
| 07c10f6542 | |||
| 2fca018add | |||
| ee6def1932 | |||
| 2004bf197a | |||
| 26173b3926 | |||
| 2a9664dbe0 | |||
| 67cc5eafad | |||
| 3663301101 | |||
| 4351ee9706 | |||
| 181b1e446e | |||
| daf093d51c | |||
| 4ae9d53e72 | |||
| 7eb672bee5 | |||
| 67e2a37149 | |||
| 92b4428146 | |||
| 9fcc70cf1c | |||
| 5e64ac3250 | |||
| 63b2a34cc8 | |||
| d81a88b607 | |||
| a223a2bc65 | |||
| cc2ccec9b1 | |||
| 4a011f65bd | |||
| b74a97016e | |||
| ae25111175 | |||
| 523bb8f10b | |||
| fba399670d | |||
| 2dbac07bf4 | |||
| b4cfc54aa2 | |||
| afb0791ea8 | |||
| 85530d36f6 | |||
| 111d7922a9 | |||
| ce8c4d18a1 | |||
| 4d8754f037 | |||
| b0399d93cc | |||
| d3df5a8606 | |||
| bf0555fb31 | |||
| b76cf098b7 | |||
| 9af1034a88 | |||
| 892ef59608 | |||
| a34e012eee | |||
| e8e7a2ec40 | |||
| 78f5863947 | |||
| 6db7c12c49 | |||
| 86ac9a2b96 | |||
| 77b825a064 | |||
| 76dab8d865 | |||
| e55e0d6707 | |||
| 6f3dda117e | |||
| f3e8026501 | |||
| 91f59d8554 | |||
| 4098cd4922 | |||
| 230222f8a7 | |||
| d8a0a4539d | |||
| 183fbdaafb | |||
| 5d8588ef0a | |||
| 6d6d4d9a5b | |||
| ab1a636ecb | |||
| 1ca07f1321 | |||
| 627f1bc7ed | |||
| eb607d34b8 | |||
| c2f2f92236 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -23,3 +23,6 @@ Desktop.ini
|
||||
/dist/subscription-build-linux-x64
|
||||
/server/AyaNova/wwwroot
|
||||
/logfile
|
||||
build-output.txt
|
||||
# compiled copy of the manuals' MkDocs hook (docs/8.0/rev_date.py)
|
||||
__pycache__/
|
||||
|
||||
6
.vscode/launch.json
vendored
6
.vscode/launch.json
vendored
@@ -24,7 +24,7 @@
|
||||
"preLaunchTask": "build debug",
|
||||
// If you have changed target frameworks, make sure to update the program path.
|
||||
//#### THIS MEANS YOU JOHNNY! If the debugger fails this is the issue
|
||||
"program": "${workspaceFolder}/server/AyaNova/bin/Debug/net8.0/AyaNova.dll",
|
||||
"program": "${workspaceFolder}/server/AyaNova/bin/Debug/net10.0/AyaNova.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/server/AyaNova",
|
||||
"stopAtEntry": false,
|
||||
@@ -55,7 +55,6 @@
|
||||
"env": {
|
||||
//############ PERPETUAL BLOCK ######################
|
||||
// "ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"AYANOVA_JWT_SECRET": "1111111MyRandom32CharacterSecret",
|
||||
"AYANOVA_LOG_LEVEL": "Info",
|
||||
//"AYANOVA_LOG_LEVEL": "Debug",
|
||||
//"AYANOVA_LOG_LEVEL": "Trace",
|
||||
@@ -94,7 +93,7 @@
|
||||
"preLaunchTask": "build debug subscription",
|
||||
// If you have changed target frameworks, make sure to update the program path.
|
||||
//#### THIS MEANS YOU JOHNNY! If the debugger fails this is the issue
|
||||
"program": "${workspaceFolder}/server/AyaNova/bin/Debug/net8.0/AyaNova.dll",
|
||||
"program": "${workspaceFolder}/server/AyaNova/bin/Debug/net10.0/AyaNova.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/server/AyaNova",
|
||||
"stopAtEntry": false,
|
||||
@@ -111,7 +110,6 @@
|
||||
//############ SUBSCRIPTION MODE BLOCK ######################
|
||||
// "ASPNETCORE_ENVIRONMENT": "Development",
|
||||
//"AYANOVA_REPORT_RENDERING_TIMEOUT":"1",
|
||||
"AYANOVA_JWT_SECRET": "1111111MyRandom32CharacterSecret",
|
||||
"AYANOVA_LOG_LEVEL": "Info",
|
||||
//"AYANOVA_LOG_LEVEL": "Debug",
|
||||
//"AYANOVA_LOG_LEVEL": "Trace",
|
||||
|
||||
42
CLAUDE.md
42
CLAUDE.md
@@ -20,15 +20,13 @@ hardcode these paths: do not move or rename the folders.
|
||||
|
||||
| Folder | What | Stack |
|
||||
|---|---|---|
|
||||
| `raven` | **This repo, the master.** Back-end server, customer manual, build and installer scripts | ASP.NET Core 8, C#, EF Core 8, Npgsql, PostgreSQL |
|
||||
| `raven` | **This repo, the master.** Back-end server, customer manual, build and installer scripts | ASP.NET Core 10, C#, EF Core 10, Npgsql, PostgreSQL |
|
||||
| `raven-client` | Front end, in `ayanova/` | Vue 2.6, Vuetify 2.6, Vuex, Vue Router, plain JavaScript, Node 12.22.9 |
|
||||
| `raven-test-integration` | API integration tests against a running server | xUnit, .NET 8 |
|
||||
| `raven-launcher` | Windows standalone launcher | .NET 8, self-contained, trimmed |
|
||||
| `raven-test-integration` | API integration tests against a running server | xUnit, .NET 10 |
|
||||
| `raven-launcher` | Windows standalone launcher | .NET 10, self-contained, trimmed |
|
||||
| `ravenqbi` | QuickBooks Desktop integration | .NET Framework 4.8 WinForms, x86, Visual Studio only |
|
||||
| `sockeye` | Internal licensing server, a near-copy of this codebase, **already ported to .NET 10** (tag `8.1.0`) **[unverified: folder name]** | same as `raven` |
|
||||
|
||||
Cypress E2E tests (Cypress 9.7.0, targeting remote devtest servers) also exist
|
||||
**[unverified: which folder]**.
|
||||
| `raven-test` | Cypress E2E suite in `e2e/` (Cypress 9.7.0, targets remote devtest servers); also JMeter (`load/`) and Locust (`loadlocust/`) load tests | Node, Cypress |
|
||||
| `sockeye` | Internal licensing server, a near-copy of this codebase, **already ported to .NET 10** (tag `8.1.0`, baseline tag `net6-baseline`) | same as `raven` |
|
||||
|
||||
## Layout of this repo
|
||||
|
||||
@@ -38,17 +36,18 @@ Cypress E2E tests (Cypress 9.7.0, targeting remote devtest servers) also exist
|
||||
- `dist/` — packaging assets, Inno Setup scripts, build outputs.
|
||||
- `build-release.bat` — master release build.
|
||||
|
||||
## Current work: .NET 10 port, branch `net10-port`
|
||||
## Current work: release 8.3.1, on `main`
|
||||
|
||||
Move the server from .NET 8 to .NET 10 and every back-end package to its current release.
|
||||
Front end: minimal or no change.
|
||||
8.3.0 (the .NET 10 port, case 4690) was released 2026-10-29, tagged `8.3.0` in `raven`,
|
||||
`raven-client` and `raven-launcher`. 8.3.1 is a patch release of fixes; new features are planned
|
||||
for a later 8.4.0. Each case has its own commits on `main` (`case NNNN - …`) and, if customers
|
||||
would notice it, a line under "AyaNova 8.3.1" in the changelog. Release steps:
|
||||
`devdocs/build/RELEASE-PROCEDURE.md`.
|
||||
|
||||
**Before any port work, read `devdocs/modernization/NET10-PORT.md`.** It holds the plan,
|
||||
the checklist and the decisions still open. Update its checklist and log in the same
|
||||
commit as the change they record.
|
||||
`devdocs/modernization/NET10-PORT.md` is the record of 8.3.0. Its Deferred list holds candidate
|
||||
cases for later releases: when one is done, mark it there with its case number in the same commit.
|
||||
|
||||
Sockeye went through the same port first. When a port question comes up, look at how
|
||||
Sockeye solved it: `git -C ..\sockeye log --oneline net6-baseline..8.1.0`.
|
||||
Sockeye went through the same port first: `git -C ..\sockeye log --oneline net6-baseline..8.1.0`.
|
||||
|
||||
The rest of the modernization plan: `devdocs/modernization/ROADMAP.md`.
|
||||
|
||||
@@ -56,10 +55,11 @@ The rest of the modernization plan: `devdocs/modernization/ROADMAP.md`.
|
||||
|
||||
- No change to production code without John's explicit approval. Propose a plan first for anything beyond a one-line fix.
|
||||
- One concern per commit, so a regression can be found with `git log` and undone with `git revert`. The version bump is always the first commit on a release branch.
|
||||
- Do not mix refactoring, bug fixes and features. If you spot something worth refactoring during the port, add it to "Deferred" in `NET10-PORT.md` instead of doing it.
|
||||
- Dependency changes: back-end packages only, and only on `net10-port`. No front-end dependency changes (Node is pinned at 12.22.9 because of `fibers`).
|
||||
- Do not mix refactoring, bug fixes and features. If you spot something worth refactoring, add it to "Deferred" in `NET10-PORT.md` instead of doing it.
|
||||
- Dependency changes: back-end packages only, and only with John's approval (the port that updated them all is done). No front-end dependency changes (Node is pinned at 12.22.9 because of `fibers`).
|
||||
- Do not add `[ApiController]` to controllers that lack it. It changes model binding and error responses on routes deployed clients call.
|
||||
- No new features.
|
||||
- Low-level server fixes (`util`, `generator`, `ControllerHelpers`, `DataList`, startup): check whether Sockeye needs the same fix with `..\sockeye\devdocs\compare-with-raven.sh` (names mapped in `NAME-MAP.md` there), and record the answer.
|
||||
- Git: keep it simple and explicit. Do not push, tag, merge or rewrite history unless asked. Tags are annotated and pushed by name.
|
||||
- Nothing is accepted blindly: show the diff and say why.
|
||||
- Secrets and customer data never go in a repo. Fixtures that contain customer data live outside the repo and are found through an environment variable.
|
||||
@@ -67,7 +67,7 @@ The rest of the modernization plan: `devdocs/modernization/ROADMAP.md`.
|
||||
## Environment
|
||||
|
||||
- Windows 11, PowerShell, VS Code. Visual Studio only for `ravenqbi`.
|
||||
- .NET 10 SDK installed alongside the .NET 8 runtime **[unverified: check `dotnet --info`]**.
|
||||
- .NET SDK 10.0.402 (installed with winget) and 10.0.103 (from Visual Studio 2026); no `global.json`, so builds use the newest. No 8 SDK (the 10 SDK still builds `net8.0`). Runtimes 8.0.24, 10.0.3 and 10.0.13 (also 6.0.36).
|
||||
- Local server: `http://localhost:7575`, API base `/api/v8/`.
|
||||
- Compile-time constant `SUBSCRIPTION_BUILD` (`-p:SUBSCRIPTION_BUILD=true`) distinguishes the hosted build; use `#if SUBSCRIPTION_BUILD`.
|
||||
|
||||
@@ -93,8 +93,10 @@ dotnet publish server\AyaNova\AyaNova.csproj -c Release -r linux-x64 --no-self-c
|
||||
|---|---|
|
||||
| `devdocs/modernization/NET10-PORT.md` | The current port: plan, checklist, open questions, log |
|
||||
| `devdocs/modernization/ROADMAP.md` | Modernization steps and their status |
|
||||
| `devdocs/modernization/SECURITY-REVIEW.md` | Whole-codebase security review in passes: scope, threat model, route map, findings, pass log. Review only, fixes become cases |
|
||||
| `devdocs/modernization/PROJECT-OVERVIEW.md` | Products, deployment targets, business context |
|
||||
| `devdocs/modernization/CODEBASE-REFERENCE.md` | Stack, structure, configuration, API conventions |
|
||||
| `devdocs/modernization/TESTING.md` | Test suites and their state |
|
||||
| `devdocs/build/BUILD.md` | Release build, packaging, version numbers |
|
||||
| `devdocs/build/DEV-VM-SETUP.md` | Rebuilding the Windows build VM from scratch |
|
||||
| `devdocs/build/BUILD.md` | Release build, packaging |
|
||||
| `devdocs/build/RELEASE-PROCEDURE.md` | Release steps from code-complete to announcement, tagging, subscription droplet upgrades and weekly updates, version bump |
|
||||
| `devdocs/build/DEV-VM-SETUP.md` | Deprecated build VM plan; its tool list still serves for setting up a build machine. Releases are built on the workstation |
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
# Build the image:
|
||||
# docker build -f Dockerfile.docs -t ayanova-docs-build .
|
||||
#
|
||||
# Run a build (mount your docs source and output):
|
||||
# docker run --rm -v "c:/data/code/raven/docs/8.0/ayanova:/docs" -v "c:/data/code/raven/server/AyaNova/wwwroot/docs:/output" ayanova-docs-build
|
||||
# Run a build (mount docs/8.0, not only ayanova: mkdocs.yml uses the shared hook ../rev_date.py):
|
||||
# docker run --rm -v "c:/data/code/raven/docs/8.0:/docs" -w /docs/ayanova -v "c:/data/code/raven/server/AyaNova/wwwroot/docs:/output" ayanova-docs-build
|
||||
#
|
||||
# Serve docs locally for preview (on port 8000):
|
||||
# docker run --rm -p 8000:8000 -v "c:/data/code/raven/docs/8.0/ayanova:/docs" ayanova-docs-build mkdocs serve -a 0.0.0.0:8000
|
||||
# docker run --rm -p 8000:8000 -v "c:/data/code/raven/docs/8.0:/docs" -w /docs/ayanova ayanova-docs-build mkdocs serve -a 0.0.0.0:8000
|
||||
#
|
||||
|
||||
FROM python:3.11-slim-bullseye
|
||||
|
||||
206
ayanova.tests/LicenceGateTests.cs
Normal file
206
ayanova.tests/LicenceGateTests.cs
Normal file
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
Licence verification gate for the .NET 10 port (devdocs/modernization/NET10-PORT.md, section 0).
|
||||
|
||||
Runs every v8 row of Sockeye's alicense fixture through AyaNova's own License.Parse, by
|
||||
reflection, so the production verification code is tested unchanged: signature, payload
|
||||
extraction, JSON parse, build-date check (MExBB) and build-type check.
|
||||
|
||||
The fixture is customer data and stays outside the repo. Export it as described in
|
||||
sockeye\sockeye.tests\ResignTests.cs. Format 1 rows are v7 keys and are not used here.
|
||||
|
||||
Set AYANOVA_LICENCE_GATE_OUT to a file path to also write "id outcome" lines (no key text),
|
||||
so two builds can be diffed.
|
||||
|
||||
Tests reference AyaNova.csproj without SUBSCRIPTION_BUILD, so this is the perpetual build.
|
||||
*/
|
||||
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
[assembly: CollectionBehavior(DisableTestParallelization = true)]
|
||||
|
||||
namespace ayanova.tests;
|
||||
|
||||
public record LicenceRow(long Id, int Format, string Key);
|
||||
|
||||
public static class LicenceFixture
|
||||
{
|
||||
static readonly Lazy<Dictionary<long, LicenceRow>> _rows = new(Load);
|
||||
public static IReadOnlyDictionary<long, LicenceRow> V8Rows => _rows.Value;
|
||||
|
||||
static Dictionary<long, LicenceRow> Load()
|
||||
{
|
||||
var path = "C:\\data\\sockeye-fixtures\\resign.json";
|
||||
if (!File.Exists(path))
|
||||
throw new InvalidOperationException($"Licence fixture not found at {path}. Export it as described in sockeye\\sockeye.tests\\ResignTests.cs.");
|
||||
|
||||
var rows = System.Text.Json.JsonSerializer.Deserialize<List<LicenceRow>>(
|
||||
File.ReadAllText(path),
|
||||
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
|
||||
return rows.Where(r => r.Format != 1).ToDictionary(r => r.Id);
|
||||
}
|
||||
}
|
||||
|
||||
// Reflection access to AyaNova's internal licence code
|
||||
public static class AyaNovaLicence
|
||||
{
|
||||
static readonly Assembly Asm = typeof(AyaNova.Program).Assembly;
|
||||
static readonly Type LicenseType = Asm.GetType("AyaNova.Core.License", true);
|
||||
static readonly Type FileUtilType = Asm.GetType("AyaNova.Util.FileUtil", true);
|
||||
static readonly MethodInfo ParseMethod = LicenseType.GetMethod("Parse", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
static readonly MethodInfo MExBBMethod = LicenseType.GetMethod("MExBB", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
static readonly PropertyInfo ServerDbIdProperty = LicenseType.GetProperty("ServerDbId", BindingFlags.NonPublic | BindingFlags.Static);
|
||||
static readonly MethodInfo LinkerTimestampMethod = FileUtilType.GetMethod("GetLinkerTimestampUtc", BindingFlags.Public | BindingFlags.Static, new[] { typeof(Assembly) });
|
||||
|
||||
public static string MismatchToBuildError =>
|
||||
(string)LicenseType.GetField("LICENSE_MISMATCH_TO_BUILD_ERROR", BindingFlags.NonPublic | BindingFlags.Static).GetValue(null);
|
||||
|
||||
public static DateTime BuildTimestampUtc => (DateTime)LinkerTimestampMethod.Invoke(null, new object[] { Asm });
|
||||
|
||||
public static void SetServerDbId(string dbId) => ServerDbIdProperty.SetValue(null, dbId);
|
||||
|
||||
// Returns the parsed key object, or throws the exception Parse threw (unwrapped from reflection)
|
||||
public static object Parse(string keyText)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ParseMethod.Invoke(null, new object[] { keyText, NullLogger.Instance });
|
||||
}
|
||||
catch (TargetInvocationException e)
|
||||
{
|
||||
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(e.InnerException).Throw();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool MExBB(DateTime buildDate, object key) => (bool)MExBBMethod.Invoke(null, new object[] { buildDate, key });
|
||||
|
||||
public static object NewKey() => Activator.CreateInstance(LicenseType.GetNestedType("AyaNovaLicenseKey", BindingFlags.NonPublic), true);
|
||||
|
||||
public static object Get(object o, string name) => o.GetType().GetProperty(name).GetValue(o);
|
||||
|
||||
public static void Set(object o, string name, object value) => o.GetType().GetProperty(name).SetValue(o, value);
|
||||
}
|
||||
|
||||
public class LicenceGateTests
|
||||
{
|
||||
const string OK = "ok";
|
||||
const string MISMATCH = "mismatch-to-build";
|
||||
|
||||
public static IEnumerable<object[]> RowIds() =>
|
||||
LicenceFixture.V8Rows.Keys.Select(id => new object[] { id });
|
||||
|
||||
[Fact]
|
||||
public void Fixture_has_v8_rows()
|
||||
{
|
||||
Assert.NotEmpty(LicenceFixture.V8Rows);
|
||||
Assert.All(LicenceFixture.V8Rows.Values, r => Assert.Equal("8", (string)KeyJson(r)["Key"]["LicenseFormat"]));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(RowIds))]
|
||||
public void Stored_v8_licence_verifies(long id)
|
||||
{
|
||||
var row = LicenceFixture.V8Rows[id];
|
||||
var json = KeyJson(row)["Key"];
|
||||
AyaNovaLicence.SetServerDbId((string)json["DBID"]);
|
||||
|
||||
var expected = ExpectedOutcome(json);
|
||||
string actual;
|
||||
object parsed = null;
|
||||
try
|
||||
{
|
||||
parsed = AyaNovaLicence.Parse(row.Key);
|
||||
actual = OK;
|
||||
}
|
||||
catch (ApplicationException e) when (e.InnerException?.Message == AyaNovaLicence.MismatchToBuildError)
|
||||
{
|
||||
actual = MISMATCH;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Signature, format, parse or server-match failure: never acceptable for a stored key
|
||||
actual = $"error: {e.InnerException?.Message ?? e.Message}";
|
||||
}
|
||||
|
||||
RecordOutcome(id, actual);
|
||||
Assert.Equal(expected, actual);
|
||||
|
||||
if (parsed != null)
|
||||
{
|
||||
Assert.Equal((string)json["RegisteredTo"], (string)AyaNovaLicence.Get(parsed, "RegisteredTo"));
|
||||
Assert.Equal((string)json["DBID"], (string)AyaNovaLicence.Get(parsed, "DbId"));
|
||||
Assert.Equal((bool?)json["Perpetual"] ?? true, (bool)AyaNovaLicence.Get(parsed, "Perpetual"));
|
||||
Assert.Equal((DateTime)json["LicenseExpiration"], (DateTime)AyaNovaLicence.Get(parsed, "LicenseExpiration"));
|
||||
Assert.Equal((DateTime)json["MaintenanceExpiration"], (DateTime)AyaNovaLicence.Get(parsed, "MaintenanceExpiration"));
|
||||
var features = (System.Collections.IList)AyaNovaLicence.Get(parsed, "Features");
|
||||
Assert.Equal(((JArray)json["Features"]).Count, features.Count);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tampered_v8_licence_fails_signature()
|
||||
{
|
||||
var row = LicenceFixture.V8Rows.Values.First();
|
||||
AyaNovaLicence.SetServerDbId((string)KeyJson(row)["Key"]["DBID"]);
|
||||
// Any change to the signed payload must fail the signature check
|
||||
var tampered = row.Key.Replace("KEY]", "xKEY]");
|
||||
var e = Assert.Throws<ApplicationException>(() => AyaNovaLicence.Parse(tampered));
|
||||
Assert.Contains("integrity check", e.InnerException.Message);
|
||||
}
|
||||
|
||||
// Guards the build date that MExBB compares against: it comes from the PE linker timestamp,
|
||||
// which is only a real date while AyaNova.csproj has <Deterministic>False</Deterministic>
|
||||
[Fact]
|
||||
public void Build_timestamp_is_a_real_recent_date()
|
||||
{
|
||||
var built = AyaNovaLicence.BuildTimestampUtc;
|
||||
Assert.InRange(built, new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), DateTime.UtcNow.AddMinutes(5));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MExBB_compares_maintenance_with_build_date()
|
||||
{
|
||||
var built = AyaNovaLicence.BuildTimestampUtc;
|
||||
var key = AyaNovaLicence.NewKey();
|
||||
AyaNovaLicence.Set(key, "Perpetual", true);
|
||||
|
||||
AyaNovaLicence.Set(key, "MaintenanceExpiration", built.AddDays(-1));
|
||||
Assert.True(AyaNovaLicence.MExBB(built, key));
|
||||
|
||||
AyaNovaLicence.Set(key, "MaintenanceExpiration", built.AddDays(1));
|
||||
Assert.False(AyaNovaLicence.MExBB(built, key));
|
||||
}
|
||||
|
||||
// What production must do with this key on a perpetual build, worked out from the key itself
|
||||
static string ExpectedOutcome(JToken json)
|
||||
{
|
||||
if ((string)json["RegisteredTo"] == "REVOKED")
|
||||
return OK;
|
||||
var perpetual = (bool?)json["Perpetual"] ?? true;
|
||||
var trial = ((JArray)json["Features"]).Any(f => string.Equals((string)f["Name"], "TrialMode", StringComparison.OrdinalIgnoreCase));
|
||||
var maintenanceBeforeBuild = perpetual && !trial && (DateTime)json["MaintenanceExpiration"] < AyaNovaLicence.BuildTimestampUtc;
|
||||
if (maintenanceBeforeBuild || !perpetual)
|
||||
return MISMATCH;
|
||||
return OK;
|
||||
}
|
||||
|
||||
static JObject KeyJson(LicenceRow row)
|
||||
{
|
||||
var a = row.Key.IndexOf("[KEY", StringComparison.Ordinal) + 4;
|
||||
var b = row.Key.IndexOf("KEY]", StringComparison.Ordinal);
|
||||
return JObject.Parse(row.Key.Substring(a, b - a));
|
||||
}
|
||||
|
||||
static readonly object OutLock = new();
|
||||
static void RecordOutcome(long id, string outcome)
|
||||
{
|
||||
var path = Environment.GetEnvironmentVariable("AYANOVA_LICENCE_GATE_OUT");
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
return;
|
||||
lock (OutLock)
|
||||
File.AppendAllText(path, $"{id} {outcome}{Environment.NewLine}");
|
||||
}
|
||||
}
|
||||
24
ayanova.tests/ayanova.tests.csproj
Normal file
24
ayanova.tests/ayanova.tests.csproj
Normal file
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.10.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="4.0.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\server\AyaNova\AyaNova.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,3 +1,13 @@
|
||||
@rem Runs itself again with --logged so all output also goes to build-output.txt beside this script (overwritten each build, not in git)
|
||||
@if /i "%~1"=="--logged" goto LOGGED
|
||||
@powershell -NoProfile -ExecutionPolicy Bypass -Command "[Console]::OutputEncoding = [Text.Encoding]::UTF8; cmd /c '%~f0' --logged '2>&1' | Tee-Object -FilePath '%~dp0build-output.txt'"
|
||||
@pause
|
||||
@exit /b
|
||||
:LOGGED
|
||||
@chcp 65001 >nul
|
||||
rem mkdocs-material prints a warning banner MkDocs cannot write to a pipe without UTF-8
|
||||
set PYTHONUTF8=1
|
||||
|
||||
set m=Unknown error
|
||||
|
||||
@echo ****************** CLEAN ALL OUTPUT FOLDERS **********
|
||||
@@ -69,17 +79,6 @@ set m=BUILD PERPETUAL LINUX x64 FAILED
|
||||
goto FAIL
|
||||
)
|
||||
|
||||
@REM ### DISCONTINUED INDEFINITELY ###
|
||||
@REM rem LINUX DOCKER
|
||||
@REM rmdir C:\data\code\raven\dist\docker\linux-x64\ayanovadocker\files /s/q
|
||||
@REM mkdir C:\data\code\raven\dist\docker\linux-x64\ayanovadocker\files
|
||||
@REM xcopy C:\data\code\raven\dist\linux-x64\* C:\data\code\raven\dist\docker\linux-x64\ayanovadocker\files /s /e
|
||||
@REM "C:\Program Files\7-Zip\7z.exe" a -tzip "C:\data\code\raven\dist\installers\ayanova-linux-docker-x64-server.zip" "C:\data\code\raven\dist\docker\linux-x64\*" -r
|
||||
@REM IF %ERRORLEVEL% NEQ 0 (
|
||||
@REM set m=BUILD LINUX x64 DOCKER ZIP FAILED
|
||||
@REM goto FAIL
|
||||
@REM )
|
||||
|
||||
rem LINUX DESKTOP
|
||||
"C:\Program Files\7-Zip\7z.exe" a -tzip "C:\data\code\raven\dist\installers\ayanova-linux-x64-desktop.zip" "C:\data\code\raven\dist\linux-x64\*" -r
|
||||
"C:\Program Files\7-Zip\7z.exe" a -tzip "C:\data\code\raven\dist\installers\ayanova-linux-x64-desktop.zip" "C:\data\code\raven\dist\assets\linux-desktop\config.json"
|
||||
@@ -158,7 +157,7 @@ echo.
|
||||
echo.
|
||||
echo Error:
|
||||
echo %m%
|
||||
pause
|
||||
rem the outer run pauses once the log is complete
|
||||
goto DONE
|
||||
|
||||
::------------------------
|
||||
@@ -169,7 +168,6 @@ echo.
|
||||
echo.
|
||||
echo.
|
||||
@echo Completed without error
|
||||
pause
|
||||
|
||||
|
||||
:DONE
|
||||
@@ -3,7 +3,7 @@
|
||||
@echo **************************************************************
|
||||
cd C:\data\code\raven
|
||||
call code .
|
||||
cd C:\data\code\raven-client\ayanova
|
||||
cd C:\data\code\raven-client
|
||||
call code .
|
||||
start cmd /k Call C:\data\code\raven-client\ayanova\appdev.bat
|
||||
start "" /D C:\data\code\raven-client\ayanova cmd /k Call C:\data\code\raven-client\ayanova\appdev.bat
|
||||
start cmd /k Call C:\data\code\raven\startsql.bat
|
||||
|
||||
@@ -1,16 +1,27 @@
|
||||
# AyaNova v8 release build
|
||||
|
||||
How a full release is built: tools, folder layout, what `build-release.bat` does, and where
|
||||
version numbers live. Written for .NET 8 (8.2.4); the .NET 10 port updates it.
|
||||
version numbers live. Written for .NET 8 (8.2.4); updated for .NET 10 (8.3.0).
|
||||
|
||||
To set up a machine that can run this from scratch, see `DEV-VM-SETUP.md`.
|
||||
Releases are built on the workstation; the backup laptop is the fallback. To set up a machine
|
||||
that can run this from scratch, use the tool list and install commands in `DEV-VM-SETUP.md`
|
||||
(the VM itself is deprecated).
|
||||
|
||||
**Backup laptop: still to bring in line with the workstation** (John's notes, 2026-10-23):
|
||||
|
||||
- [ ] Install the .NET 10 SDK: `winget install --id Microsoft.DotNet.SDK.10 -e --source winget` (the .NET 8 SDK is no longer needed)
|
||||
- [ ] Replace KeePass and KeeAgent with KeePassXC
|
||||
- [ ] Install rclone and configure the S3 remote: an rclone remote named `spaces` for DigitalOcean Spaces (bucket `gztw1`)
|
||||
- [ ] Copy over the backup batch file `C:\data\biz-servers\weekly-copy-biz-server-backup-locally.bat` (run weekly: copies the Sockeye and Gitea backups from Spaces into `C:\data\biz-servers\data-backup-local-copy`)
|
||||
- [ ] Uninstall FileZilla
|
||||
- [ ] Fix up TortoiseGit and Git SSH so both work
|
||||
|
||||
## What gets built
|
||||
|
||||
| Project | Framework | Output |
|
||||
|---|---|---|
|
||||
| AyaNova server (`raven/server/AyaNova`) | .NET 8 | Five packages, below |
|
||||
| Launcher (`raven-launcher`) | .NET 8, self-contained, `PublishTrimmed` | Inside the Windows standalone installer |
|
||||
| AyaNova server (`raven/server/AyaNova`) | .NET 10 | Five packages, below |
|
||||
| Launcher (`raven-launcher`) | .NET 10, self-contained, `PublishTrimmed` | Inside the Windows standalone installer |
|
||||
| QBI (`ravenqbi/AyaNovaQBI`) | .NET Framework 4.8 | `ayanova-qbi-setup.exe`, built separately |
|
||||
|
||||
| Package | Build |
|
||||
@@ -25,13 +36,13 @@ To set up a machine that can run this from scratch, see `DEV-VM-SETUP.md`.
|
||||
|
||||
| Tool | Version | Used for |
|
||||
|---|---|---|
|
||||
| .NET SDK | 8.0.x | Server and launcher |
|
||||
| .NET SDK | 10.0.x, newest | Server and launcher. **Update before every release build** (`winget install --id Microsoft.DotNet.SDK.10 -e`, or `winget upgrade` once installed that way): the Windows standalone server and the launcher are self-contained and ship the .NET runtime of the SDK that built them (SDK 10.0.103 → runtime 10.0.3; 10.0.402 → 10.0.13). The SDK that comes with Visual Studio is not updated by winget |
|
||||
| Node.js | **12.22.9 exactly** | Front end. Newer versions break `fibers` |
|
||||
| Python | 3.11 | MkDocs |
|
||||
| MkDocs + mkdocs-material | current | Customer manual (`pip install mkdocs mkdocs-material`) |
|
||||
| MkDocs + mkdocs-material | MkDocs 1.x | Customer manual (`pip install "mkdocs<2" mkdocs-material`). Material for MkDocs does not work with MkDocs 2.0: keep the pin until the docs move to Zensical (see `../modernization/ROADMAP.md`) |
|
||||
| Inno Setup | 6.x | Windows installers |
|
||||
| 7-Zip | current | Linux zips |
|
||||
| Visual Studio | **[unverified: 2022 or 18/2026]** | QBI only |
|
||||
| Visual Studio | 18 (2026) Community on the dev machine; no VS 2022 installed | QBI only |
|
||||
|
||||
The scripts hardcode these paths:
|
||||
|
||||
@@ -84,7 +95,7 @@ customer portal docs).
|
||||
|
||||
### Windows installers
|
||||
|
||||
- **Standalone** (`standalone.iss`): self-contained server, launcher, embedded PostgreSQL, empty database template, desktop shortcut to the launcher. App in `{autopf}\ayanova`; data in `{commonappdata}\ayanova\database`; logs in `{commonappdata}\ayanova\logs`. The launcher starts PostgreSQL, starts the server, opens the browser at the login page and waits.
|
||||
- **Standalone** (`standalone.iss`): self-contained server, launcher, embedded PostgreSQL, empty database template, desktop shortcut to the launcher. App in `{autopf}\ayanova`; data in `{commonappdata}\ayanova\database`; logs in `{commonappdata}\ayanova\logs`. The launcher starts PostgreSQL, starts the server, opens the browser at the login page and waits; when the server exits or its window is closed it stops PostgreSQL with `pg_ctl stop` (case 4694). Setup and uninstall also stop a running embedded PostgreSQL first (8.2.4 and older launchers left it running, which blocked upgrades with "files in use"). `postgres-standalone\bin` includes the Visual C++ runtime DLLs PostgreSQL needs (`msvcp140.dll`, `vcruntime140.dll`, `vcruntime140_1.dll`, 14.44.35211.0, Microsoft-signed, app-local; case 4695): Windows Update never updates app-local copies, so replace them when PostgreSQL is updated.
|
||||
- **LAN** (`lan.iss`): framework-dependent server plus network config. No PostgreSQL. Prompts the user to download the ASP.NET Core Hosting Bundle and PostgreSQL.
|
||||
|
||||
### QBI (separate)
|
||||
@@ -101,15 +112,8 @@ timestamp utility.
|
||||
|
||||
## Version numbers
|
||||
|
||||
Set in five places for every release:
|
||||
|
||||
| File | Setting | At 8.2.4 |
|
||||
|---|---|---|
|
||||
| `raven\server\AyaNova\AyaNova.csproj` | `<Version>` | 8.2.4 |
|
||||
| `raven\dist\install\windows\x64\standalone.iss` | `#define MyAppVersion` | 8.2.4 |
|
||||
| `raven\dist\install\windows\x64\lan.iss` | `#define MyAppVersion` | 8.2.4 |
|
||||
| `raven-client\ayanova\package.json` | `"version"` | 8.2.4 |
|
||||
| `ravenqbi\install\qbi.iss` | `#define MyAppVersion` | 8.0.2 (own cycle) |
|
||||
The full list of files to change, and when, is in `RELEASE-PROCEDURE.md`, step 12 (version bump).
|
||||
QBI has its own cycle: `ravenqbi\install\qbi.iss`, `#define MyAppVersion` (8.0.2).
|
||||
|
||||
## Front-end build notes
|
||||
|
||||
@@ -119,8 +123,8 @@ Set in five places for every release:
|
||||
|
||||
## Docker builds
|
||||
|
||||
`Dockerfile.frontend` and `Dockerfile.docs` build the front end and the manual in containers
|
||||
**[unverified: where they live]**. The Linux server packages and the Windows
|
||||
`raven-client\ayanova\Dockerfile.frontend` and `raven\Dockerfile.docs` build the front end
|
||||
and the manual in containers. The Linux server packages and the Windows
|
||||
framework-dependent build could also build in Docker. Windows self-contained builds, Inno
|
||||
Setup, QBI and the embedded PostgreSQL need Windows.
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
# AyaNova v8 build VM setup
|
||||
|
||||
> **Deprecated (October 2026).** The plan to develop and build entirely in a VM was dropped: it could not cover the QuickBooks (QBI) project and some other parts. Releases are built directly on the workstation, with the backup laptop as the fallback (see `../modernization/PROJECT-OVERVIEW.md`). The tool list and install commands below are still a usable checklist for setting up either machine.
|
||||
|
||||
> **Purpose**: Step-by-step instructions to create a portable Windows virtual machine containing the complete AyaNova v8 build environment. This VM can run on Windows or Linux hosts.
|
||||
|
||||
Written February 2026 for the .NET 8 build. The .NET 10 port adds the .NET 10 SDK (see `../modernization/NET10-PORT.md`, section 5).
|
||||
Written February 2026 for the .NET 8 build; updated October 2026 for the .NET 10 port (AyaNova 8.3.0): the .NET 10 SDK replaces the .NET 8 SDK.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
@@ -247,8 +250,8 @@ winget install --id Microsoft.VisualStudioCode -e --source winget
|
||||
# Python 3.11
|
||||
winget install --id Python.Python.3.11 -e --source winget
|
||||
|
||||
# .NET 8 SDK
|
||||
winget install --id Microsoft.DotNet.SDK.8 -e --source winget
|
||||
# .NET 10 SDK (server, launcher and integration tests all target net10.0 from 8.3.0)
|
||||
winget install --id Microsoft.DotNet.SDK.10 -e --source winget
|
||||
|
||||
# Inno Setup
|
||||
winget install --id JRSoftware.InnoSetup -e --source winget
|
||||
@@ -272,7 +275,7 @@ node -v
|
||||
### 4.5 Install MkDocs
|
||||
|
||||
```powershell
|
||||
pip install mkdocs mkdocs-material
|
||||
pip install "mkdocs<2" mkdocs-material
|
||||
```
|
||||
|
||||
Verify:
|
||||
@@ -636,7 +639,7 @@ msbuild C:\data\code\ravenqbi\AyaNovaQBI\AyaNovaQBI.csproj /p:Configuration=Rele
|
||||
|------|------|
|
||||
| 7-Zip | `C:\Program Files\7-Zip\7z.exe` |
|
||||
| Inno Setup | `C:\Program Files (x86)\Inno Setup 6\ISCC.exe` |
|
||||
| MSBuild | `C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe` **[unverified]**: 6.3 was changed to `18` (Visual Studio 2026) while 4.6 still says install VS 2022. Confirm which is installed |
|
||||
| MSBuild | `C:\Program Files\Microsoft Visual Studio\18\Community\MSBuild\Current\Bin\MSBuild.exe`. Confirmed on the dev machine (VS 18 / 2026; no VS 2022). Section 4.6 still says install VS 2022: install VS 2026 instead |
|
||||
|
||||
---
|
||||
|
||||
|
||||
389
devdocs/build/RELEASE-PROCEDURE.md
Normal file
389
devdocs/build/RELEASE-PROCEDURE.md
Normal file
@@ -0,0 +1,389 @@
|
||||
# AyaNova v8 release and server update procedure
|
||||
|
||||
How a release goes from finished code to customers, and how the hosted subscription droplets are
|
||||
updated every week. Replaces John's notes scattered across info cases (the Subversion-era
|
||||
release notes and the subscription upgrade scripts of info case 3450). The build itself (tools,
|
||||
`build-release.bat`, packages) is in `BUILD.md`; release-specific extras (for 8.3.0: the .NET 10
|
||||
runtime, the one-time droplet hardening) are in that release's checklist
|
||||
(`devdocs/modernization/NET10-PORT.md`, section 5).
|
||||
|
||||
Every command is a complete line for the shell named above it: PowerShell on the workstation,
|
||||
bash on a droplet or the ayanova.com web server (logged in as root).
|
||||
|
||||
## Release order at a glance
|
||||
|
||||
1. Code complete: every case committed and pushed, local E2E passes
|
||||
2. Docs: changelog (manual and client copyright years are automatic)
|
||||
3. Build with `build-release.bat`
|
||||
4. Stage the installers and docs in the `next` folders on ayanova.com
|
||||
5. Test on devtest: upgrade it from `next`, remote E2E, check each change
|
||||
6. Tag the release in git
|
||||
7. Publish: docs and downloads go live (not announced yet)
|
||||
8. Download every published file back and check its version
|
||||
9. **Upgrade the subscription droplets** (Thursday, by time zone)
|
||||
10. **Announce**: upload `latest-version.json`
|
||||
11. Tell subscription customers
|
||||
12. Start the next version
|
||||
|
||||
**Subscription droplets are upgraded before the release is announced** (John, 2026-10-26, from
|
||||
8.3.0 on). Uploading `latest-version.json` is what tells every AyaNova installation that a new
|
||||
version exists, so it is the very last publishing step. Between steps 7 and 10 the new docs and
|
||||
downloads are already visible on the website; that gap is accepted.
|
||||
|
||||
## 1. Code complete
|
||||
|
||||
- All "next release" cases done, each committed with its case number and pushed, in every repo.
|
||||
- Every repo clean and in step with its remote. All four must say `nothing to commit, working tree clean` and `up to date with 'origin/main'`:
|
||||
|
||||
```powershell
|
||||
git -C C:\data\code\raven status; git -C C:\data\code\raven-client status; git -C C:\data\code\raven-launcher status; git -C C:\data\code\raven-test-integration status
|
||||
```
|
||||
|
||||
- Integration tests pass against the local server (`raven-test-integration`, see `devdocs/modernization/TESTING.md`).
|
||||
- E2E passes against the local debug server (API on 7575, client dev server on 8080). In `raven-test\e2e`, `cypress.json` `expectedServerVersion` must be the new version:
|
||||
|
||||
```powershell
|
||||
cd C:\data\code\raven-test\e2e; .\local-run.bat
|
||||
```
|
||||
|
||||
## 2. Docs
|
||||
|
||||
**Often missed: check carefully against the previous release.**
|
||||
|
||||
- `docs/8.0/ayanova/docs/changelog.md`: the new version's section is complete and reads well; remove "IN PROGRESS".
|
||||
- The new version's section starts with `Released YYYY-MM-DD`, the planned publication date (step 7), as every earlier version does. The manual is built into every package, so it must be right before the build.
|
||||
- Manual revision stamp and copyright year: set automatically when the manuals are built
|
||||
(`docs/8.0/rev_date.py`, the date of the last commit that changed each manual). After the build,
|
||||
check the bottom of a page in `server/AyaNova/wwwroot/docs`: `REV-` must be that date.
|
||||
- Copyright year on the login page: set automatically when the client is built (`raven-client`
|
||||
`ayanova/vue.config.js`, the year of the build).
|
||||
- `dist/latest-version.json`: the new version and its changelog anchor (for example `#ayanova-830`). Set at the version bump; check it here.
|
||||
|
||||
## 3. Build
|
||||
|
||||
Before the build, update the .NET SDK so the Windows standalone package and the launcher carry the
|
||||
newest .NET runtime (they bundle the runtime of the SDK that builds them; see `BUILD.md`, Tools).
|
||||
If winget finds nothing newer, nothing changes:
|
||||
|
||||
```powershell
|
||||
winget upgrade --id Microsoft.DotNet.SDK.10 -e --source winget
|
||||
```
|
||||
|
||||
Then run the full build; it must end with "COMPLETED WITHOUT ERROR". Details in `BUILD.md`.
|
||||
|
||||
```powershell
|
||||
cd C:\data\code\raven; .\build-release.bat
|
||||
```
|
||||
|
||||
Write down the commit each repo was built from (the tag in step 6 goes on exactly these):
|
||||
|
||||
```powershell
|
||||
git -C C:\data\code\raven log --oneline -1; git -C C:\data\code\raven-client log --oneline -1; git -C C:\data\code\raven-launcher log --oneline -1
|
||||
```
|
||||
|
||||
## 4. Stage in `next`
|
||||
|
||||
On the ayanova.com web server (`ssh root@ayanova.com`), empty the previous release's `next` folders:
|
||||
|
||||
```bash
|
||||
rm -r /var/www/html/ayanova.com/download/next/* /var/www/html/ayanova.com/docs-next/*
|
||||
```
|
||||
|
||||
Then, from PowerShell on the workstation, upload the installers and the built manual (each
|
||||
command passes `scp` the full list of files and folders to copy):
|
||||
|
||||
```powershell
|
||||
scp (Get-ChildItem C:\data\code\raven\dist\installers -File).FullName root@ayanova.com:/var/www/html/ayanova.com/download/next/
|
||||
```
|
||||
|
||||
```powershell
|
||||
scp -r (Get-ChildItem C:\data\code\raven\server\AyaNova\wwwroot\docs).FullName root@ayanova.com:/var/www/html/ayanova.com/docs-next/
|
||||
```
|
||||
|
||||
## 5. Test on devtest
|
||||
|
||||
devtest.onayanova.com mirrors the customer droplets; it is the final confirmation before any
|
||||
customer is touched. For earlier, riskier tests, create a temporary droplet when needed and
|
||||
destroy it when done, so it does not run up costs (for 8.3.0 that was `devtestn10`).
|
||||
|
||||
1. Bring its OS up to date as in the [weekly update](#weekly-os-update), reboot.
|
||||
2. Any one-time steps this release needs (for 8.3.0: [the .NET 10 runtime and hardening](#one-time-steps-for-830)).
|
||||
3. Upgrade it with the [next version upgrade commands](#upgrade-commands).
|
||||
4. Remote E2E:
|
||||
|
||||
```powershell
|
||||
cd C:\data\code\raven-test\e2e; .\remote-run.bat
|
||||
```
|
||||
|
||||
5. Check each item in the changelog on it, and that the new version number shows everywhere (login page, About form).
|
||||
|
||||
If anything fails: fix it as a case, rebuild (step 3), restage (step 4) and test again. Nothing
|
||||
is tagged or published until devtest passes.
|
||||
|
||||
## 6. Tag the release in git
|
||||
|
||||
A tag is a permanent name on one commit, so `8.3.0` always points at exactly the code that was
|
||||
shipped, years later and whatever happens on `main` afterwards. An **annotated** tag (`-a`) also
|
||||
records who tagged it, when, and a message. Tags are not sent by an ordinary `git push`, so each
|
||||
one is pushed by name. Tag the repos whose code is in the release, at the commits noted in step 3
|
||||
(if nothing was committed since, that is the current `main`):
|
||||
|
||||
```powershell
|
||||
git -C C:\data\code\raven tag -a 8.3.0 -m "AyaNova 8.3.0"; git -C C:\data\code\raven push origin 8.3.0
|
||||
```
|
||||
|
||||
```powershell
|
||||
git -C C:\data\code\raven-client tag -a 8.3.0 -m "AyaNova 8.3.0"; git -C C:\data\code\raven-client push origin 8.3.0
|
||||
```
|
||||
|
||||
```powershell
|
||||
git -C C:\data\code\raven-launcher tag -a 8.3.0 -m "AyaNova 8.3.0"; git -C C:\data\code\raven-launcher push origin 8.3.0
|
||||
```
|
||||
|
||||
Check that the tags are on the server (each lists `refs/tags/8.3.0`):
|
||||
|
||||
```powershell
|
||||
git -C C:\data\code\raven ls-remote --tags origin; git -C C:\data\code\raven-client ls-remote --tags origin; git -C C:\data\code\raven-launcher ls-remote --tags origin
|
||||
```
|
||||
|
||||
A pushed tag is never moved or deleted: if a problem turns up after release, the fix becomes the
|
||||
next patch version (8.3.1) with its own tag. To see later what any release contained:
|
||||
`git -C C:\data\code\raven show 8.3.0`, or compare two releases with
|
||||
`git -C C:\data\code\raven log --oneline 8.2.4..8.3.0` (from 8.3.0 on; earlier releases were not tagged in git).
|
||||
|
||||
## 7. Publish (not announced yet)
|
||||
|
||||
The download folders on the ayanova.com web server:
|
||||
|
||||
- `download/` always holds the current release, plus files that are not part of a release (for
|
||||
example `QBFC14_0installer.exe`), which stay.
|
||||
- `download/next/` is staging: the release as built and tested in steps 4 and 5.
|
||||
- `download/old/` archives the three most recent releases, including the current one, each in a
|
||||
folder named for its release date and version. The current release is therefore in both
|
||||
`download/` and `download/old/`, on purpose.
|
||||
|
||||
On the ayanova.com web server, in this order:
|
||||
|
||||
1. Replace the manual with the new one:
|
||||
|
||||
```bash
|
||||
rm -r /var/www/html/ayanova.com/docs/* && cp -r /var/www/html/ayanova.com/docs-next/* /var/www/html/ayanova.com/docs
|
||||
```
|
||||
|
||||
2. Replace this release's files in `download/` with the tested ones from `next/`. Only the files
|
||||
in `next/` are overwritten; everything else in `download/` stays. The same command keeps a copy
|
||||
of the current `latest-version.json` (it is not in `next/` and is replaced only in step 10):
|
||||
|
||||
```bash
|
||||
cp /var/www/html/ayanova.com/download/next/* /var/www/html/ayanova.com/download && cp /var/www/html/ayanova.com/download/latest-version.json /var/www/html/ayanova.com/download/previous-latest-version.json
|
||||
```
|
||||
|
||||
3. Archive the release in `download/old/`, in a folder named `YYYY-MM-DD-vX.Y.Z` with the release
|
||||
date and version (for 8.3.0, released 2026-10-29):
|
||||
|
||||
```bash
|
||||
mkdir /var/www/html/ayanova.com/download/old/2026-10-29-v8.3.0 && cp /var/www/html/ayanova.com/download/next/* /var/www/html/ayanova.com/download/old/2026-10-29-v8.3.0
|
||||
```
|
||||
|
||||
4. Remove the oldest release folder in `download/old`, so three remain (kept since December 2023;
|
||||
customers whose maintenance lapsed must purchase to upgrade, and the older versions are kept
|
||||
only to diagnose a regression). List the folder, then remove the oldest; at 8.3.0 that is 8.2.2:
|
||||
|
||||
```bash
|
||||
ls /var/www/html/ayanova.com/download/old
|
||||
```
|
||||
|
||||
```bash
|
||||
rm -r /var/www/html/ayanova.com/download/old/2024-10-23-v8.2.2
|
||||
```
|
||||
|
||||
Do **not** upload the new `latest-version.json` yet.
|
||||
|
||||
## 8. Check what was published
|
||||
|
||||
Download every installer and package back from ayanova.com and check its version: Windows
|
||||
installers by the .exe's ProductVersion, Linux zips by the `AyaNova.dll` inside. (Found
|
||||
2026-10-25: the LAN installer on the website was still 8.2.3, so 8.2.4 had never been uploaded for LAN.)
|
||||
|
||||
```powershell
|
||||
$ProgressPreference = 'SilentlyContinue'; $d = "$env:TEMP\ayanova-published"; Remove-Item $d -Recurse -Force -ErrorAction SilentlyContinue; New-Item -ItemType Directory $d | Out-Null; foreach ($f in 'ayanova-windows-x64-single-setup.exe','ayanova-windows-x64-lan-setup.exe','ayanova-linux-x64-server.zip','ayanova-linux-x64-desktop.zip','ayanova-subscription-linux-x64-server.zip') { Invoke-WebRequest "https://www.ayanova.com/download/$f" -OutFile "$d\$f" -UseBasicParsing; if ($f -like '*.exe') { "$f $((Get-Item "$d\$f").VersionInfo.ProductVersion)" } else { Expand-Archive "$d\$f" "$d\$($f -replace '\.zip$','')" ; "$f $((Get-Item "$d\$($f -replace '\.zip$','')\AyaNova.dll").VersionInfo.ProductVersion)" } }
|
||||
```
|
||||
|
||||
Every line must show the new version. If one does not, fix the download folder (step 7) and check
|
||||
again; no subscription droplet is upgraded and nothing is announced until every line is right.
|
||||
|
||||
## 9. Upgrade the subscription droplets
|
||||
|
||||
On the normal Thursday schedule and times (or out of band for an emergency), as in the
|
||||
[weekly update](#weekly-os-update): snapshot, health check, OS update, reboot; then any
|
||||
one-time steps for this release, then the [release upgrade commands](#upgrade-commands); then the
|
||||
health check and the log entry. Mark each server as upgraded in its Sockeye subscription server record.
|
||||
|
||||
## 10. Announce
|
||||
|
||||
Only after the last subscription droplet is upgraded, from PowerShell on the workstation (overwrites
|
||||
the old one; from this moment every AyaNova installation shows that the new version is available):
|
||||
|
||||
```powershell
|
||||
scp C:\data\code\raven\dist\latest-version.json root@ayanova.com:/var/www/html/ayanova.com/download/
|
||||
```
|
||||
|
||||
## 11. Tell subscription customers
|
||||
|
||||
Subscription customers are told after their server was upgraded, not before. Perpetual licence
|
||||
customers see the new version in AyaNova itself; tell one only if they are waiting for a
|
||||
specific change. Message:
|
||||
|
||||
> Subject: Your AyaNova service was upgraded
|
||||
>
|
||||
> Your AyaNova service has been upgraded to the latest version.
|
||||
>
|
||||
> This release contains these features and improvements:
|
||||
> https://ayanova.com/docs/changelog/#ayanova-830
|
||||
>
|
||||
> Be sure to backup your data regularly:
|
||||
> https://ayanova.com/docs/sub-service-intro/
|
||||
|
||||
## 12. Start the next version
|
||||
|
||||
Version numbers follow [semantic versioning](https://semver.org/): MAJOR for incompatible API
|
||||
changes, MINOR for new backward compatible functionality, PATCH for backward compatible fixes.
|
||||
|
||||
The version bump is the **first commit** of the next release, `version bump X.Y.Z` (no case: a
|
||||
release has no case of its own), one commit per repo, made right after the release, before any
|
||||
new code. Set the new version in:
|
||||
|
||||
| Repo | File | What |
|
||||
|---|---|---|
|
||||
| raven | `server/AyaNova/AyaNova.csproj` | `<Version>` and `<FileVersion>` |
|
||||
| raven | `server/AyaNova/util/AyaNovaVersion.cs` | `VersionString` |
|
||||
| raven | `dist/install/windows/x64/lan.iss` | `#define MyAppVersion` |
|
||||
| raven | `dist/install/windows/x64/standalone.iss` | `#define MyAppVersion` |
|
||||
| raven | `dist/latest-version.json` | `latestversion` and the `changelogurl` anchor |
|
||||
| raven | `docs/8.0/ayanova/docs/changelog.md` | new version heading; under it the "Upgrading from 8.2.4 or older?" line copied from the previous version's section (case 4759; keep it until John decides enough customers are past 8.3.0), then "IN PROGRESS" |
|
||||
| raven | `docs/8.0/ayanova/docs/ay-about.md` | the example version number |
|
||||
| raven-client | `ayanova/package.json`, `ayanova/package-lock.json` | `"version"` (both places in the lock file) |
|
||||
| raven-client | `ayanova/src/api/ayanova-version.js` | `version` |
|
||||
| raven-launcher | `ayanova-version.cs` | `VersionString` |
|
||||
| raven-test | `e2e/cypress.json` | `expectedServerVersion` |
|
||||
|
||||
Searching each repo for the previous version number finds them all:
|
||||
|
||||
```powershell
|
||||
git -C C:\data\code\raven grep -n -F "8.3.0" -- server dist docs/8.0/ayanova/docs/ay-about.md; git -C C:\data\code\raven-client grep -n -F "8.3.0" -- ayanova/package.json ayanova/src/api; git -C C:\data\code\raven-launcher grep -n -F "8.3.0"
|
||||
```
|
||||
|
||||
QBI (`ravenqbi`) has its own version cycle (`install\qbi.iss`).
|
||||
|
||||
## Upgrade commands
|
||||
|
||||
Run on a subscription droplet as root. They stop AyaNova, download the new zip, replace the
|
||||
program files (keeping `config.json` and everything in `data`), and start AyaNova again. `curl -f`
|
||||
makes a failed download stop the whole chain before anything is deleted; without it a missing
|
||||
file would be saved as an error page, the program files deleted and the unzip would fail, leaving
|
||||
the server down until the snapshot is restored.
|
||||
|
||||
**Next version, for testing on devtest** (from the `next` folder):
|
||||
|
||||
```bash
|
||||
cd /var/ayanova && systemctl stop ayanova.service && curl -fO https://www.ayanova.com/download/next/ayanova-subscription-linux-x64-server.zip && rm *.dll && rm -r ./resource && rm -r ./wwwroot && unzip -o ayanova-subscription-linux-x64-server.zip -x "config.json" && systemctl start ayanova.service
|
||||
```
|
||||
|
||||
**Release version, for customer droplets:**
|
||||
|
||||
```bash
|
||||
cd /var/ayanova && systemctl stop ayanova.service && curl -fO https://www.ayanova.com/download/ayanova-subscription-linux-x64-server.zip && rm *.dll && rm -r ./resource && rm -r ./wwwroot && unzip -o ayanova-subscription-linux-x64-server.zip -x "config.json" && systemctl start ayanova.service
|
||||
```
|
||||
|
||||
Then `reboot`, and the health check: `https://<server>.onayanova.com/health`.
|
||||
|
||||
## One-time steps for 8.3.0
|
||||
|
||||
Every existing droplet, once, right before its 8.3.0 upgrade. Both are safe to run again.
|
||||
|
||||
**.NET 10 runtime** (8.3.0 needs it; on Ubuntu 24.04 and 26.04 it comes from Ubuntu's own archive):
|
||||
|
||||
```bash
|
||||
apt-get install -y aspnetcore-runtime-10.0
|
||||
```
|
||||
|
||||
**Hardening** (case 4727; what the setup script now does for new droplets): AyaNova listens only on
|
||||
127.0.0.1, the program folder becomes read only for the service account, `config.json` 640, the
|
||||
old 8.2.x Chrome folders are removed, and the systemd security settings are added as a drop-in.
|
||||
Tested 2026-10-26 on `devtestn10`. Save it on the droplet with `nano /root/harden-830.sh`, paste,
|
||||
save, then run `bash /root/harden-830.sh`; it must end with `HARDENING DONE`. It takes effect when
|
||||
the upgrade commands start AyaNova.
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# AyaNova 8.3.0: one-time hardening of an existing subscription droplet (case 4727, F36 and F38).
|
||||
# Run once per droplet as root, right before the 8.3.0 upgrade commands. Safe to run again.
|
||||
set -e
|
||||
|
||||
# safety copies, kept from the first run only
|
||||
[ -e /root/config.json.before-8.3.0 ] || cp /var/ayanova/config.json /root/config.json.before-8.3.0
|
||||
[ -e /root/ayanova.service.before-8.3.0 ] || cp /etc/systemd/system/ayanova.service /root/ayanova.service.before-8.3.0
|
||||
|
||||
# F36: nginx is the only way in: listen on this server only, not on its public address
|
||||
sed -i 's#http://[*]:7575#http://127.0.0.1:7575#' /var/ayanova/config.json
|
||||
grep -q '"http://127.0.0.1:7575"' /var/ayanova/config.json
|
||||
|
||||
# F38: program files read only for the service account; it writes to data only
|
||||
chmod -R go-w /var/ayanova
|
||||
chmod -R g+rw /var/ayanova/data
|
||||
# config.json holds the database password: readable by root and the service group only
|
||||
chown root:www-data /var/ayanova/config.json
|
||||
chmod 640 /var/ayanova/config.json
|
||||
# 8.2.x report browser folders; 8.3.0 downloads the browser into data/report-browser
|
||||
rm -rf /var/ayanova/Chrome /var/ayanova/ChromeHeadlessShell
|
||||
|
||||
# F38: systemd sandboxing, as a drop-in so the existing unit file is left as it is
|
||||
mkdir -p /etc/systemd/system/ayanova.service.d
|
||||
cat > /etc/systemd/system/ayanova.service.d/hardening.conf <<'EOF'
|
||||
[Service]
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=/var/ayanova/data
|
||||
PrivateTmp=true
|
||||
ProtectHome=true
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
|
||||
echo HARDENING DONE
|
||||
```
|
||||
|
||||
After the upgrade, check that AyaNova listens only on 127.0.0.1 (`ss -tln | grep 7575` shows
|
||||
`127.0.0.1:7575`) and that the health check passes.
|
||||
|
||||
## Weekly OS update
|
||||
|
||||
Every Thursday, Ubuntu updates on every subscription droplet. Customers have been told updates
|
||||
happen on Thursday nights after 7 pm their time (downtime under a minute; none has asked for
|
||||
another time). All droplets have phased updates turned off, so they get the same packages
|
||||
devtest was tested with (`/etc/apt/apt.conf.d/52no-phasing`; the setup script adds it to new droplets).
|
||||
|
||||
Order: devtest first (it mirrors the customer droplets), then customers in groups by time zone,
|
||||
at John's 7 am (Singapore, Australia), 4 pm (US east coast and Europe, most servers) and 7 pm
|
||||
(US west coast).
|
||||
|
||||
Per droplet:
|
||||
|
||||
1. In the DigitalOcean control panel: delete last week's snapshot, take a new one.
|
||||
2. Confirm the server is healthy: `https://<server>.onayanova.com/health`.
|
||||
3. SSH in, remove last week's leftovers, update, reboot (always reboot: skipping it caused intermittent problems in the past):
|
||||
|
||||
```bash
|
||||
apt autoremove
|
||||
```
|
||||
|
||||
```bash
|
||||
apt-get update && apt-get -V full-upgrade
|
||||
```
|
||||
|
||||
```bash
|
||||
reboot
|
||||
```
|
||||
|
||||
4. Copy the session's console output into the week's log file (kept until the next week's updates, to look back at if something comes up).
|
||||
5. Mark the date and time of the update in the server's Sockeye subscription server record.
|
||||
6. Refresh the server's AyaNova page and confirm the login page appears; open `/health` in place of `/login` and paste the result into the week's log as the last entry for that server.
|
||||
@@ -18,7 +18,7 @@ truth**. Anything marked **[unverified]** has not been checked against the code.
|
||||
| MailKit | 4.5.0 | Email |
|
||||
| Swashbuckle.AspNetCore | 6.5.0 | OpenAPI |
|
||||
| NLog.Web.AspNetCore | 5.3.11 | Logging |
|
||||
| Asp.Versioning.Mvc | — | API versioning **[unverified]** |
|
||||
| Asp.Versioning.Mvc | 8.1.0 | API versioning (with `Asp.Versioning.Mvc.ApiExplorer`) |
|
||||
|
||||
### Structure
|
||||
|
||||
@@ -26,14 +26,21 @@ truth**. Anything marked **[unverified]** has not been checked against the code.
|
||||
raven/server/AyaNova/
|
||||
├── AyaNova.csproj
|
||||
├── Program.cs, Startup.cs
|
||||
├── biz/ business objects (…Biz.cs), report rendering (ReportBiz.cs)
|
||||
├── ControllerHelpers/
|
||||
├── Controllers/ API controllers
|
||||
├── DataList/ data-list definitions and the SQL filter builder
|
||||
├── generator/ background jobs (CoreJob…: backup, licence, sweeper …)
|
||||
├── kpi/
|
||||
├── models/ EF Core context and entities
|
||||
├── PickList/
|
||||
├── util/ ServerBootConfig, DbUtil, License, FileUtil, Mailer …
|
||||
├── resource/ report templates and static resources, copied to output
|
||||
│ └── rpt/ report templates (.ayrt)
|
||||
└── wwwroot/ built front end and customer manual, copied in by the build
|
||||
```
|
||||
|
||||
Older notes also list `Controllers/`, `Models/`, `Services/` and `Data/` folders
|
||||
**[unverified]**: that list reads like a generic template, not the actual layout.
|
||||
Data-list classes live in the `AyaNova.DataList` namespace, and their class names are the
|
||||
There are no `Services/` or `Data/` folders. Data-list classes live in the `AyaNova.DataList` namespace, and their class names are the
|
||||
list keys (`WorkOrderDataList`, `CustomerDataList` …).
|
||||
|
||||
### Hosted vs self-hosted
|
||||
@@ -50,19 +57,24 @@ list keys (`WorkOrderDataList`, `CustomerDataList` …).
|
||||
|
||||
### Reports
|
||||
|
||||
PuppeteerSharp renders PDFs in headless Chrome, downloaded on first run. A customer
|
||||
setting holds extra browser launch parameters, and an environment variable can point at
|
||||
an existing browser instead of downloading one (`REPORT_RENDER_BROWSER_PATH` in older
|
||||
notes **[unverified]**).
|
||||
PuppeteerSharp renders PDFs in headless Chrome, downloaded on first run
|
||||
(`biz/ReportBiz.cs`). Two configuration keys control it: `AYANOVA_REPORT_RENDER_BROWSER_PATH`
|
||||
points at an existing browser instead of downloading one, and
|
||||
`AYANOVA_REPORT_RENDER_BROWSER_PARAMS` replaces the default launch arguments
|
||||
(`util/ServerBootConfig.cs:84-88`, `:185-191`).
|
||||
|
||||
### Configuration
|
||||
|
||||
Older notes list `ConnectionStrings__DefaultConnection`, `JWT_SECRET` and
|
||||
`ASPNETCORE_ENVIRONMENT` as the server's settings **[unverified]**. Sockeye, the near-copy,
|
||||
reads `SOCKEYE_*` keys from a `config.json` beside the app (`SOCKEYE_DB_CONNECTION`,
|
||||
`SOCKEYE_USE_URLS`, `SOCKEYE_JWT_SECRET`, `SOCKEYE_DATA_PATH` …), so AyaNova likely does the
|
||||
same with its own prefix. The Linux packages ship a `config.json` from
|
||||
`dist/assets/linux-server/` or `dist/assets/linux-desktop/`, which supports that.
|
||||
`Program.cs:38` reads `config.json` beside the app, then environment variables, then the
|
||||
command line (later sources win). `util/ServerBootConfig.cs` reads `AYANOVA_*` keys:
|
||||
`AYANOVA_DB_CONNECTION`, `AYANOVA_USE_URLS`, `AYANOVA_JWT_SECRET`, `AYANOVA_DATA_PATH`,
|
||||
`AYANOVA_LOG_PATH`, `AYANOVA_LOG_LEVEL`, `AYANOVA_ATTACHMENT_FILES_PATH`,
|
||||
`AYANOVA_BACKUP_FILES_PATH`, `AYANOVA_TEMP_FILES_PATH`, `AYANOVA_BACKUP_PG_DUMP_PATH`,
|
||||
`AYANOVA_DEFAULT_TRANSLATION`, `AYANOVA_SET_SUPERUSER_PW`, `AYANOVA_REPORT_RENDER_*`, plus
|
||||
boot flags such as `AYANOVA_PERMANENTLY_ERASE_DATABASE` and `AYANOVA_REMOVE_LICENSE_FROM_DB`.
|
||||
The names in older notes (`ConnectionStrings__DefaultConnection`, `JWT_SECRET`) were wrong.
|
||||
The Linux packages ship a `config.json` from `dist/assets/linux-server/` or
|
||||
`dist/assets/linux-desktop/`; the LAN installer uses `dist/assets/lan-install-config.json`.
|
||||
|
||||
### API conventions
|
||||
|
||||
|
||||
@@ -9,9 +9,10 @@ under "Deferred", not into the branch.
|
||||
| Starting point | AyaNova 8.2.4: ASP.NET Core 8, EF Core 8 |
|
||||
| Target | `net10.0`, every back-end package at its current release |
|
||||
| Front end | Minimal or no change. Rebuild only if needed (open question) |
|
||||
| Branch | `net10-port` in `raven` (and `raven-client` only if the client changes) |
|
||||
| Branch | `net10-port` in `raven` (and `raven-client` and `raven-test-integration` and `raven-launcher`) |
|
||||
| Baseline tag | `net8-baseline`, on the last commit before the port |
|
||||
| Release version | Not yet decided (open question) |
|
||||
| Release version | 8.3.0 |
|
||||
| Release timing | Tentatively within two weeks of 2026-10-23, after John's move of Sockeye and the ayanova.com business server to a new droplet (weekend of 2026-10-24) has settled. The case database's "next release" cases (including the SuperUser delete bug) must be done first. The security review of the whole codebase (`SECURITY-REVIEW.md`) runs before 8.3.0 and its findings gate the release (John, 2026-10-23); after 8.3.0: an MCP interface for AyaNova 8 |
|
||||
|
||||
## Where this plan comes from
|
||||
|
||||
@@ -45,100 +46,109 @@ them. Items touching those three areas carry more weight here than they did in S
|
||||
|
||||
- Version bump first. One concern per commit.
|
||||
- Prove each gate on the **unmodified** code before changing anything, so a later failure points at the port, not at the test.
|
||||
- Packages that must move together: framework-matched `Microsoft.*` packages all go to 10.x in one step. `BouncyCastle.Cryptography` 2.x goes with MailKit/MimeKit 4.x, because MimeKit 3.x pulls in `Portable.BouncyCastle` and the assemblies clash.
|
||||
- Packages that must move together: framework-matched `Microsoft.*` packages all go to 10.x in one step. (The BouncyCastle/MimeKit clash from Sockeye does not arise here: AyaNova is already on `BouncyCastle.Cryptography` 2.x with MailKit/MimeKit 4.x.)
|
||||
- Publish `server\AyaNova\AyaNova.csproj` directly, never the solution, so no test project can reach the artifact.
|
||||
|
||||
---
|
||||
|
||||
## 0. Before changing anything
|
||||
|
||||
- [ ] Record any port work already done in earlier sessions (branch, commits) here first, and reconcile it with this list
|
||||
- [ ] `raven` and `raven-client` clean on `main` and pushed
|
||||
- [ ] Annotated tag `net8-baseline` on the pre-port commit in `raven` and `raven-client`, pushed by name (`git push origin net8-baseline`), checked with `git ls-remote --tags origin`
|
||||
- [ ] Branch `net10-port` created and pushed with upstream
|
||||
- [ ] **First commit: version bump** in every place listed in `devdocs/build/BUILD.md` → "Version numbers"
|
||||
- [ ] Dev machine has the .NET 10 SDK and keeps the .NET 8 runtime for baseline comparisons
|
||||
- [x] Record any port work already done in earlier sessions (branch, commits) here first, and reconcile it with this list. Done: branch `net10-port` and the 8.3.0 version bump (`c2f2f922`); no code changes yet
|
||||
- [x] `raven` and `raven-client` clean on `main` and pushed. Checked 2026-10-23: in both repos `main` = `origin/main` = `net8-baseline` (no port work on `main`)
|
||||
- [x] Annotated tag `net8-baseline` on the pre-port commit in `raven` and `raven-client`. On the remote in both (`git ls-remote --tags origin`, 2026-10-23)
|
||||
- [x] Branch `net10-port` created and pushed with upstream (`origin/net10-port` exists in `raven`)
|
||||
- [x] **First commit: version bump** in every place listed in `devdocs/build/BUILD.md` → "Version numbers" (`c2f2f922`, 8.3.0)
|
||||
- [x] Dev machine has the .NET 10 SDK and keeps the .NET 8 runtime for baseline comparisons. SDK 10.0.103 (no 8 SDK; the 10 SDK builds `net8.0`); runtimes 8.0.24 and 10.0.3. Since 2026-10-23 also SDK 10.0.402 with runtime 10.0.13 (section 5)
|
||||
|
||||
### Gates (prove on the unmodified code)
|
||||
|
||||
- [ ] **Integration suite baseline.** Run `raven-test-integration` against the unmodified build and record the counts in the log below. Expected: everything passes except the three known server-side failures listed in that repo's CLAUDE.md
|
||||
- [ ] **Licence verification gate.** Find out exactly how AyaNova 8 verifies a v8 licence: library, public key, and when it re-checks. If it uses BouncyCastle, write a test that runs every v8 row of Sockeye's `alicense` fixture through AyaNova's own verification routine. The fixture is customer data: it stays outside the repo and is found through an environment variable, as Sockeye's re-sign harness does with `SOCKEYE_RESIGN_FIXTURE`. Must be green on unmodified code. A regression here would reject licences customers already have installed
|
||||
- [ ] **Report baseline.** Render the advanced reports on the unmodified build and keep the PDFs for comparison. Sockeye shares only one report with AyaNova, so Sockeye's testing says little about AyaNova's reports
|
||||
- [ ] **E2E baseline.** Cypress run against a devtest server on the unmodified build
|
||||
- [x] **Integration suite baseline.** Run `raven-test-integration` against the unmodified build and record the counts in the log below. Expected: everything passes except the three known server-side failures listed in that repo's CLAUDE.md. Result: 103 tests, 96 passed, 2 failed (`ContractCrud.CRUD`, `CustomerWithWorkOrders_CannotBeDeleted`), 5 skipped (including `DirectMessage_SendAndDelete`). Needs a freshly generated test database: a reused one fails 32 tests at the `superuser` login
|
||||
- [x] **Licence verification gate.** Write a test that runs every v8 row of Sockeye's `alicense` fixture through AyaNova's own verification routine. The fixture is customer data: it stays outside the repo, at `C:\data\sockeye-fixtures\resign.json` (a fixed path, as in Sockeye's `sockeye.tests`; the environment variable was dropped there). Must be green on unmodified code. A regression here would reject licences customers already have installed. **Done:** `ayanova.tests\LicenceGateTests.cs` (not in `raven.sln`; run with `dotnet test ayanova.tests`). It calls the production `License.Parse` by reflection for each of the 726 v8 rows, and checks each outcome against what the key itself requires on a perpetual build (verify and parse, or rejection as not licensed for this build). Plus a tamper test, a build-timestamp sanity check and an `MExBB` check. Set `AYANOVA_LICENCE_GATE_OUT` to write per-row outcomes (ids only) for diffing two builds. How AyaNova verifies:
|
||||
- **Library:** BouncyCastle (`util/License.cs:15-17`). `PemReader` reads a SubjectPublicKeyInfo PEM (`BEGIN PUBLIC KEY`, not PKCS#1) into an `AsymmetricKeyParameter`; `SignerUtilities.GetSigner("SHA256WITHRSA")` verifies (`License.cs:968-978`)
|
||||
- **Public key:** hardcoded RSA-2048 PEM, "our real 2016 public key", `License.cs:957-965`
|
||||
- **Signed payload:** the JSON between `[KEY` and `KEY]`, with whitespace outside strings stripped by regex (`License.cs:950`); base64 signature between `[SIGNATURE` and `SIGNATURE]` (`:951`)
|
||||
- **Other checks in `Parse`** (`License.cs:926-1062`): `LicenseFormat == "8"`; `DBID` equals the server's schema id; perpetual key only on a perpetual build and subscription key only on a subscription build; `MExBB`, which rejects a perpetual non-trial key whose maintenance expiry is before **the build date, read from the PE linker timestamp** (`util/FileUtil.cs:861-882`). That timestamp is only a real date because `<Deterministic>False</Deterministic>` (`AyaNova.csproj:12`). Turning determinism on, or getting the CA2022 change at `FileUtil.cs:875` wrong, would reject perpetual licences with E1020. The gate must cover `MExBB`, not only the signature
|
||||
- **When it runs:** at boot, `BootSafetyCheck` before any schema update (`Startup.cs:603`), then `InitializeAsync` (`Startup.cs:633`); after every fetch, `FetchKeyAsync` → `InstallAsync` → `InitializeAsync`, either manually (`LicenseController.cs:125`) or from `CoreJobLicense` (`generator/CoreJobLicense.cs:80`). Fetch interval (`CoreJobLicense.cs:21-34`): perpetual build 30 min while unlicensed, trial or expired, otherwise 24 h; subscription build 30 min; DEBUG 1 or 5 min. The installed key is not re-verified on a schedule; expiry is checked in CoreJobSweeper, according to the comment at `CoreJobLicense.cs:10`
|
||||
- **Test access:** `Parse` is `private static` and depends on the static `ServerDbId` and on the running assembly's timestamp. The test needs `InternalsVisibleTo` or reflection
|
||||
- [x] **Report baseline.** Waived (John). Render the advanced reports on the unmodified build and keep the PDFs for comparison. Sockeye shares only one report with AyaNova, so Sockeye's testing says little about AyaNova's reports. **John's decision (2026-10-22): no recorded baseline; John verifies the reports visually himself** (see section 4)
|
||||
- [x] **E2E baseline.** Waived (John, 2026-10-22: not practical at this point); replaced by the Cypress run on the .NET 10 build, still open in section 4. Was: Cypress run (`raven-test\e2e`) against a devtest server on the unmodified build
|
||||
|
||||
## 1. Framework and packages
|
||||
|
||||
- [ ] `net8.0` → `net10.0`, with every framework-matched `Microsoft.*` package to 10.x in the same step
|
||||
- [ ] **Hosting model.** `WebHost`, `IWebHost` and `WebHostBuilder` are obsolete in .NET 10 (ASPDEPR008, ASPDEPR004). `Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(web => web.UseStartup<Startup>())` keeps the `Startup` class unchanged
|
||||
- [ ] **Asp.Versioning.** Older notes say AyaNova already uses `Asp.Versioning.Mvc` **[unverified]**. If not: rename from `Microsoft.AspNetCore.Mvc.Versioning`; `ApiVersion` moves to `Asp.Versioning`; wiring is `services.AddApiVersioning(...).AddMvc().AddApiExplorer(...)`. Leave AV0014 warnings: **do not add `[ApiController]`**
|
||||
- [ ] **Npgsql / EF Core 10.**
|
||||
- Check `Npgsql.EnableLegacyTimestampBehavior`. Sockeye's was commented out. If AyaNova sets it, `DateTime.Kind` is a real port item
|
||||
- Check for `ConfigureWarnings(w => w.Throw())`. If present, every new EF warning becomes an exception naming its ID: fix the query, or move that ID to `.Log(...)`
|
||||
- Exercise LINQ-based queries screen by screen. Data-list filters build raw SQL and are unaffected
|
||||
- [ ] **BouncyCastle.** If AyaNova references `BouncyCastle`, it becomes `BouncyCastle.Cryptography` 2.x, moved together with MailKit/MimeKit 4.x. In Sockeye, `PemReader` still yields an `AsymmetricCipherKeyPair` for a PKCS#1 key
|
||||
- [ ] **Swashbuckle 10 / OpenAPI.NET 2.** `Microsoft.OpenApi.Models` merged into `Microsoft.OpenApi`. Bearer requirement becomes `c.AddSecurityRequirement(document => new OpenApiSecurityRequirement { [new OpenApiSecuritySchemeReference("Bearer", document)] = [] })`. No explicit `Microsoft.OpenApi` reference
|
||||
- [ ] **NLog 6.** `ArchiveNumbering` is gone; `NLogBuilder.ConfigureNLog` is replaced by `NLog.LogManager.Configuration = logConfig`. Archive numbers keep incrementing and the un-numbered file is always current. If the log viewer in the UI sorts by file name, the order breaks past 99
|
||||
- [ ] **Newtonsoft.Json.** `Mvc.NewtonsoftJson` 10.x sets a 13.0.3 floor (NU1605). Pin it as a direct reference
|
||||
- [ ] **JwtBearer 10.x**, which also retires the vulnerable IdentityModel 6.10.0 dependency if AyaNova still resolves it
|
||||
- [ ] **SMTP.** Any use of `System.Net.Mail.SmtpClient` (SYSLIB0014) moves to MailKit 4.x, as in Sockeye and contact
|
||||
- [ ] **PuppeteerSharp → 25.x.** Reporting is AyaNova's main Puppeteer surface
|
||||
- `BrowserFetcher.DefaultChromiumRevision` is gone: call `DownloadAsync()` with no argument
|
||||
- `IJSHandle.ExecutionContext` is gone: evaluate on the handle. Evaluate results are `JsonElement?`
|
||||
- [x] `net8.0` → `net10.0`, with every framework-matched `Microsoft.*` package to 10.x in the same step (`AyaNova.csproj:3`). Done for the server: JwtBearer, Mvc.NewtonsoftJson, EntityFrameworkCore, ApiDescription.Server, HealthChecks.EntityFrameworkCore → 10.0.13; Npgsql.EntityFrameworkCore.PostgreSQL → 10.0.3 (required by EF 10); `.vscode/launch.json` program paths → `net10.0`. Builds with no new code changes. Launcher: next item
|
||||
- [x] `raven-launcher`: `net8.0` → `net10.0` (no package references). `raven-launcher` `d2079c9`
|
||||
- [x] **Hosting model.** n/a: `Program.cs:330-351` already uses `Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(...)` with `UseStartup<Startup>`. Only a commented-out `BuildWebHost` line remains (`:308`)
|
||||
- [x] **Asp.Versioning.** Done: 8.1.0 → 10.2.1 (as Sockeye), explicit `.AddMvc()` added between `AddApiVersioning` and `AddApiExplorer`. New analyzer suggestions AV0015 (use `UrlSegmentApiVersionReader` only) and AV0016 (`AssumeDefaultVersionWhenUnspecified`) deliberately not acted on: both would change which requests deployed clients can make. Was already: `Asp.Versioning.Mvc` and `.ApiExplorer` 8.1.0 (`AyaNova.csproj:36-37`), wired at `Startup.cs:200-217` as `AddApiVersioning(...).AddApiExplorer(...)`. Leave AV0014 warnings: **do not add `[ApiController]`**
|
||||
- [x] **Npgsql / EF Core 10.** EF Core 10.0.13, Npgsql EF 10.0.3 (item above)
|
||||
- [x] `Npgsql.EnableLegacyTimestampBehavior`: n/a, commented out (`Program.cs:26`)
|
||||
- [x] `ConfigureWarnings(w => w.Throw())` **is present** (`Startup.cs:179`). Every new EF warning becomes an exception naming its ID: fix the query, or move that ID to `.Log(...)`. **No EF warning has been raised as an error** on .NET 10 (checked 2026-10-23): the integration suite at every step, both droplets (24.04 and 26.04, including 22/22 reports), the `devtestn10` upgrade and the Windows Sandbox installs. Searched the local dev-server and Sandbox logs and the droplet logs: no "An error was generated for warning"; the only EF entries are expected `DbUpdateException`s from integration tests that deliberately break a constraint, and connection retries while PostgreSQL was starting
|
||||
- [x] Exercise LINQ-based queries screen by screen. Through the API: the integration suite (CRUD on every tested object) at every step. Through the UI: moved to the Cypress E2E item in section 4. Data-list filters build raw SQL and are unaffected
|
||||
- [x] **BouncyCastle.** Already `BouncyCastle.Cryptography` 2.3.1 (`AyaNova.csproj:28`) with MailKit/MimeKit 4.5.0; no `Portable.BouncyCastle` in the graph. Version bump only: now 2.7.0, moved together with MailKit 4.18.1, as in Sockeye. The Sockeye note about `PemReader` and PKCS#1 key pairs does not apply: AyaNova reads a public key (see the licence gate)
|
||||
- [x] **Swashbuckle 10 / OpenAPI.NET 2.** Done: all six `Swashbuckle.AspNetCore*` 6.5.0 → 10.3.1 (Sockeye: 10.2.3), explicit `Microsoft.OpenApi` reference removed (now 2.7.5 transitively), and the three code changes below. Applies. `Microsoft.OpenApi.Models` merged into `Microsoft.OpenApi`: `using` at `Startup.cs:15` and `ConfigureSwaggerOptions.cs:6`. Bearer requirement (`Startup.cs:235-244`) becomes `c.AddSecurityRequirement(document => new OpenApiSecurityRequirement { [new OpenApiSecuritySchemeReference("Bearer", document)] = [] })`. Remove the explicit `Microsoft.OpenApi` 1.6.14 reference (`AyaNova.csproj:47`)
|
||||
- [x] **NLog 6.** Done: NLog and NLog.Web.AspNetCore 6.2.2; `ArchiveNumbering` removed; **`ArchiveFileName` kept and `ArchiveSuffixFormat = "-{0}"` added** (`Program.cs`); log viewer sorts archives by file date, newest first. Tested in a scratch program and on the server, against a folder seeded with NLog 5 archives `-0` to `-8`: without `ArchiveFileName`, NLog 6 writes straight into numbered files (no `log-ayanova.txt`) and appends to the old `-8`; with it but no suffix format, archives become `log-ayanova_NN.txt` (NLog 6 ignores `{#}`) and the old `-0` to `-8` are never deleted. Sockeye 8.1.0 ships that second form. With both settings: `log-ayanova.txt` stays current, archives are `log-ayanova-N.txt` as the manual (`docs/.../ops-log.md`) says, numbering continues after the old files, and the old files are cleaned up. `NLogBuilder.ConfigureNLog` is already replaced (`Program.cs:206`, `LogManager.Setup().LoadConfiguration`). `ArchiveNumbering = Rolling` (`Program.cs:136`) is gone in 6. Archive numbers keep incrementing and the un-numbered file is always current. **The log viewer sorts by file name as a string** (`LogFilesController.cs:120`, `OrderByDescending(z => z)`), so `-10` sorts after `-2`. Fix the sort in the same commit
|
||||
- [x] **Newtonsoft.Json.** `Mvc.NewtonsoftJson` 10.x sets a 13.0.3 floor (NU1605). Already resolved 13.0.3 transitively; now a direct reference at 13.0.4 (current; Sockeye pins 13.0.3)
|
||||
- [x] **JwtBearer 10.x.** Done: 10.0.13 with the framework step (first item); login tokens still `HS256` and accepted on every authenticated call (log, 2026-10-22). The IdentityModel part is n/a: it resolves 7.1.2, not the vulnerable 6.10.0
|
||||
- [x] **SMTP.** n/a: `util/Mailer.cs` already uses MailKit's `SmtpClient` (`:1`, `:84`); no `System.Net.Mail`
|
||||
- [x] **PuppeteerSharp 17 → 25.x.** Reporting is AyaNova's main Puppeteer surface (`biz/ReportBiz.cs`). Done: 25.15.0 (Sockeye: 25.12.0); compiles with no API changes; launch arguments fixed (below). Downloads Chrome for Testing 155 (was 124). The download location and failure handling are separate section 3 items
|
||||
- [x] `BrowserFetcher.DefaultChromiumRevision`: n/a, already `DownloadAsync()` with no argument (`ReportBiz.cs:800`, case 4586)
|
||||
- [x] `IJSHandle.ExecutionContext`: n/a, the only use is commented out (`ReportBiz.cs:858-860`). Evaluate results are `JsonElement?`: check any remaining `Evaluate*` call sites
|
||||
- [x] **Launch `chrome-headless-shell`, not full Chrome** (found on the Ubuntu 24.04 droplet). Full Chrome 155 would not start under the `www-data` service: `chrome_crashpad_handler: --database is required`, because Chrome now insists on writing config and crash-reporter files under the user's home (`/var/www`, not writable); Chrome 124 in 8.2.4 did not. Measured on the 1 GB droplet printing a report-sized page, 5 runs each: full Chrome (with writable folders) 4.1–4.9 s and about 1.6 GB summed RSS per render, headless shell 1.0–1.5 s and about 620 MB, PDFs within 0.2% in size. A browser starts for every render, so the shell is chosen on merit (John's decision). Also `XDG_CONFIG_HOME`/`XDG_CACHE_HOME` are pointed at `report-browser/browser-home` (temp folder when no data path) so a full Chrome given in `AYANOVA_REPORT_RENDER_BROWSER_PATH` starts too
|
||||
- The default download is Chrome for Testing, into `Chrome/` and `ChromeHeadlessShell/` beside the app. AyaNova 8.2.0 already moved from Chromium to Chrome to follow Puppeteer's default, so the browser choice is settled
|
||||
- Launch arguments must be separate array elements (`--no-sandbox` on its own). **This includes the report-render browser params setting** customers can edit
|
||||
- [x] Launch arguments must be separate array elements. Fixed: defaults are `new[] { "--no-sandbox" }` (`--headless` dropped, `LaunchOptions.Headless` already adds it, as in Sockeye); the customer setting is split on whitespace. A customer value containing a quoted argument with spaces would now be split; none is known. **Browser default locale:** the old `--headless` argument ran Chrome's old headless mode, which takes its default language from the OS (en-CA → Chrome's en-GB, day-first); current Chrome headless always defaults to en-US. Report scripts that format without a locale (stock sample report 80 calls `toLocaleDateString()` bare) switched from 22/10/2026 to 10/22/2026. Fixed by adding `--lang=<server CultureInfo.CurrentCulture>` unless the customer setting already has a `--lang`; skipped when the server culture is invariant. Stock helpers in `ay-report.js` pass the client's language explicitly and were never affected. **Both paths were wrong:** the defaults are one element `"--headless --no-sandbox "` (`ReportBiz.cs:812`, `:829`), and `AYANOVA_REPORT_RENDER_BROWSER_PARAMS` is passed whole as one element (`:819-822`)
|
||||
- AppArmor restricts unprivileged user namespaces on Ubuntu 24.04+, which affects the Chrome sandbox
|
||||
- [ ] Remaining packages to current releases
|
||||
- [ ] Warnings: CA2022 → `ReadExactly`; ASP0019 → header indexer instead of `Headers.Append`
|
||||
- [ ] **MiniProfiler.** Confirm it is absent: `git grep -i -e miniprofiler -e mini-profiler -e StackExchange.Profiling` in **both** `raven` and `raven-client`
|
||||
- [ ] **BinaryFormatter.** Confirm it is unused, or that `EnableUnsafeBinaryFormatterSerialization` is already false
|
||||
- [ ] **`System.GC.Server`.** Keep the Web SDK default (`true`). On one vCPU the runtime uses workstation GC anyway, and DATAS keeps server GC lean since .NET 9
|
||||
- [ ] **`InvariantGlobalization`.** Decide on EF Core string comparison and date-parsing grounds. Still open in Sockeye too
|
||||
- [x] Remaining packages to current releases. **Done:** Bogus 35.5.1 → 35.6.5 (evaluation seeder), jose-jwt 5.0.0 → 5.3.0 (login tokens), QRCoder 1.5.1 → 1.8.1 (2FA setup QR). `dotnet list package --outdated`: nothing; `--vulnerable --include-transitive`: none. MailKit 4.5.0 carried a moderate advisory (NU1902); on `net10.0` the audit also reported high-severity NU1903 for `MimeKit` 4.5.0 and transitive `System.Security.Cryptography.Pkcs` 8.0.0. **Mail done:** MailKit/MimeKit 4.18.1, BouncyCastle 2.7.0, and `System.Security.Cryptography.Pkcs` 10.0.13 pinned directly (MimeKit only requires 10.0.0, which has advisories). `dotnet list package --vulnerable --include-transitive`: none. `System.Text.Json` 5.0.2 is no longer in the graph (`dotnet nuget why`: no dependency, 2026-10-23)
|
||||
- [x] Warnings: CA2022 done with `ReadAtLeast(bytes, bytes.Length, throwOnEndOfStream: false)` rather than `ReadExactly`, which would throw on a short file on the licence path. ASP0019: n/a. It flags `Headers.Add` and recommends `Append` or the indexer; `Startup.cs` already uses `Append` and no ASP0019 warning is reported (the line below had it backwards). Was: CA2022 → `ReadExactly` at `FileUtil.cs:875` (**licence build-date path**: covered by the licence gate); ASP0019 → header indexer instead of `Headers.Append` at `Startup.cs:514`
|
||||
- [x] **MiniProfiler. Present, not absent. Removed** (John's decision, as in Sockeye `6dd2b09`): both packages, `AddMemoryCache`/`AddMiniProfiler`/`UseMiniProfiler`, the `/profiler/results` download-token workaround in the auth middleware, the unused `using` in `ReportBiz.cs`, three commented-out `using` lines, and the credit in `docs/.../appendix-os-credits.md`. Client: `ops-profile.vue` and its route remain, but the menu entry was already commented out (`initialize.js:785`) and the page was retired after the beta (`changelog.md:519`); John may remove them. Was: `MiniProfiler.AspNetCore.Mvc` and `.EntityFrameworkCore` 4.3.8 (`AyaNova.csproj:48-49`); `AddMiniProfiler(...).AddEntityFramework()` (`Startup.cs:62-74`); auth workaround for `/profiler/results` (`Startup.cs:393-441`); `UseMiniProfiler()` (`Startup.cs:530`); `using StackExchange.Profiling.Internal` (`ReportBiz.cs:14`). Nothing in `raven-client`. Keep and upgrade, or remove: see Open questions
|
||||
- [x] **BinaryFormatter.** n/a: not used anywhere in `server/AyaNova`
|
||||
- [x] **`System.GC.Server`.** n/a: not set, so the Web SDK default (`true`) stays. On one vCPU the runtime uses workstation GC anyway, and DATAS keeps server GC lean since .NET 9
|
||||
- [x] **`InvariantGlobalization`.** Decision (John, 2026-10-22): leave it off. The server formats with the OS culture (the report browser `--lang` depends on it), and turning it on would change date and number handling. Was: Not set. Decide on EF Core string comparison and date-parsing grounds. Note `DataListSqlFilterCriteriaBuilder.cs:835` reads `CurrentCulture.NumberFormat`. Still open in Sockeye too
|
||||
|
||||
## 2. Configuration behaviour to check
|
||||
|
||||
- [ ] **URL binding precedence.** An in-code `UseUrls` beats `ASPNETCORE_URLS`. Find where AyaNova takes its URLs from (Sockeye: `SOCKEYE_USE_URLS` in `config.json`) and make sure the documented setting is the one that wins
|
||||
- [ ] **Logging providers.** `appsettings.json` very likely sets the Debug and Console providers to `Trace`, as Sockeye's does. Check whether `Program.cs` calls `ClearProviders()` before adding NLog. If it does not, `Trace` floods the journal under systemd. Fix the levels in the repo either way
|
||||
- [ ] **Data Protection keys.** Only matters if AyaNova encrypts anything with Data Protection and runs under a service account with no home directory: then the key ring must be persisted explicitly. A JWT-only API is unaffected
|
||||
- [x] **URL binding precedence.** n/a: `UseUrls(ServerBootConfig.AYANOVA_USE_URLS)` (`Program.cs:340`), read from `config.json`, environment and command line (`Program.cs:38`, `ServerBootConfig.cs:167-170`). The documented `AYANOVA_USE_URLS` is the one that wins over `ASPNETCORE_URLS`; a command-line `--urls` still overrides it through `UseConfiguration` (`Program.cs:341`)
|
||||
- [x] **Logging providers.** `appsettings.json` Debug and Console levels `Trace` → `Warning`. `appsettings.Development.json` left: only read when `ASPNETCORE_ENVIRONMENT=Development`, which nothing sets. No journal flood: `Program.cs:348` calls `ClearProviders()` before `UseNLog()`, so the old `Trace` levels never had an effect
|
||||
- [x] **Data Protection keys.** n/a: nothing uses Data Protection; authentication is JWT only (`Startup.cs:278-302`)
|
||||
|
||||
## 3. Fixes found through Sockeye (each its own commit)
|
||||
|
||||
These are bugs, not port mechanics, so each is a separate commit that can be reverted alone.
|
||||
Sockeye included its equivalents in the port branch. John decides per item whether they go
|
||||
in this branch or a follow-up release.
|
||||
in this branch or a follow-up release. UPDATE: John has decided these all go in this branch and some more additional cases in the case database that are urgent, exact cases TBD.
|
||||
|
||||
- [ ] **Case 4676: data-list SQL injection — priority.** The numeric and role filters in `DataListSqlFilterCriteriaBuilder` built SQL from input. In AyaNova any authenticated user who can open a data list could reach them. Port the Sockeye fix
|
||||
- [ ] **`DbUtil.AdminConnectionString`.** If it is `_dbConnectionString.Replace(_dbName, "postgres")`, any customer whose role name, password or host contains the database name as text gets a mangled admin connection, and the boot check authenticates as the wrong user. Fix: `new NpgsqlConnectionStringBuilder(cs) { Database = "postgres" }.ConnectionString`
|
||||
- [ ] **Backup `pg_dump` call.** If it builds a `postgresql://user:password@host:port/db` URI with the raw password and runs it through a shell for a `> file` redirect, a password with spaces or `@ : / ? # %` breaks backups. Fix: password in `PGPASSWORD` in the process environment, connection parameters as separate arguments through `ProcessStartInfo.ArgumentList` (no shell), output with `-f`
|
||||
- [ ] **Drop-and-recreate database.** Without `CREATEDB` it drops the database and then fails to create it. Test the permission before dropping, or change the order
|
||||
- [ ] **Report browser download location.** Point it under the data directory in code. Beside the app it is read-only under a hardened systemd unit (`ProtectSystem=strict`), and a redeploy that replaces the app folder deletes it
|
||||
- [ ] **Report failures must not kill the server.** On the Sockeye destination a failed browser download was an unhandled exception that ended the whole process (systemd `core-dump`, then restart); the client saw only `TypeError … reading 'length'`. Catch download and launch failures, log them through NLog, and return an error to the client
|
||||
- [ ] **Data-list date filters, small fixes.** `>=` and `<` should use `ZeroSeconds`, not `MaxSeconds`; date `NotEqual` should exclude the whole minute (`< ZeroSeconds OR > MaxSeconds`). In DEBUG builds, `DataListField.UiFieldDataType` is read before `DataListField` is null-checked
|
||||
- [ ] **Relative date tokens use the server's time zone** (`*today*`, `*yesterday*` …), which is wrong for any user in a different zone from the server. The fix builds the range from `clientTimeStamp.Offset`, with care for ranges that cross a DST change. **This changes behaviour** and affects the integration tests' `TIME_ZONE_ADJUSTMENT`. Probably better as a follow-up release
|
||||
- [x] **Case 4676: data-list SQL injection — priority.** Confirmed: `DataListSqlFilterCriteriaBuilder` appended the raw filter value for Roles and for every numeric type (Enum, MemorySize, Decimal, Currency, InternalId, Integer). Text and tag values escape `'`; TimeSpan goes through `int.Parse`. Any authenticated user who can open a data list could reach this. **Sockeye's fix (`3427cd8`) is incomplete:** it checks only Decimal/Currency and Integer, and leaves InternalId, Enum, MemorySize and Roles open (still open in Sockeye 8.1.0). AyaNova's fix parses every numeric type as an invariant `decimal` (Decimal, Currency) or `long` (the rest), and Roles as a `long`, then appends the re-formatted number. An invalid value throws `ArgumentException`, which the API returns as an error before any SQL runs
|
||||
- [x] **`DbUtil.AdminConnectionString`.** Fixed with the builder below (Sockeye 8.1.0 still has the `Replace`). Confirmed: `DbUtil.cs:99` is `_dbConnectionString.Replace(_dbName, "postgres")`, used by the boot check (`:192`), create (`:286`) and drop/recreate (`:334`). Any customer whose role name, password or host contains the database name as text gets a mangled admin connection, and the boot check authenticates as the wrong user. Fix: `new NpgsqlConnectionStringBuilder(cs) { Database = "postgres" }.ConnectionString`
|
||||
- [x] **Backup `pg_dump` call.** Fixed (Sockeye 8.1.0 still has the old call): new `RunProgram.RunNoShell` (no shell, `ArgumentList`, per-process environment, both streams read together, timeout kills the process tree, non-zero exit code returned as an error); backup passes `--host/--port/--username/--dbname/--format=custom/--file/--no-password` with the password in `PGPASSWORD`. The old `RunProgram.Run` now has no callers (see Deferred). Confirmed: `CoreJobBackup.cs:68` builds `postgresql://user:password@host:port/db` with the raw password, and `:94` adds `-Fc > file`; `RunProgram` runs it through `cmd.exe /C` (`RunProgram.cs:55`) or `/bin/sh -c` (`:82-89`). A password with spaces or `@ : / ? # %` breaks backups. Also, on Linux stderr is not redirected (`RunProgram.cs:86-92`), so a failed pg_dump returns empty output and is **reported as a successful backup** (`CoreJobBackup.cs:97`). Fix: password in `PGPASSWORD` in the process environment, connection parameters as separate arguments through `ProcessStartInfo.ArgumentList` (no shell), output with `-f`, stderr and exit code checked
|
||||
- [x] **Drop-and-recreate database.** Fixed: before the `DROP`, `DropAndRecreateDbAsync` checks `rolcreatedb OR rolsuper` for the current user; without it, boot stops with an E1000 message naming the user and the setting, and the database is untouched. Confirmed: `DbUtil.cs:342-345` drops, then creates; triggered by `AYANOVA_PERMANENTLY_ERASE_DATABASE` (`Startup.cs:567-570`). Without `CREATEDB` it drops the database and then fails to create it. Test the permission before dropping, or change the order
|
||||
- [x] **Report browser download location.** Fixed: downloads into `<AYANOVA_DATA_PATH>/report-browser` (derived `ServerBootConfig.REPORT_RENDER_BROWSER_DOWNLOAD_PATH`, not a new setting; empty when no data path is set, then beside the app as before) through `BrowserFetcherOptions.Path`, and launches the downloaded executable explicitly. `AYANOVA_REPORT_RENDER_BROWSER_PATH` still takes precedence. Existing installs keep an unused old download beside the app until it is removed. Confirmed: `new BrowserFetcher()` with the default path beside the app (`ReportBiz.cs:800`). Point it under the data directory in code. Beside the app it is read-only under a hardened systemd unit (`ProtectSystem=strict`), and a redeploy that replaces the app folder deletes it
|
||||
- [x] **Report failures: return a specific error.** Done: download and launch each in their own `try`; on failure `HandleBrowserFailure` logs at Error (was Debug via the job processor) and writes `rendererror.exception` ("Report rendering browser could not be downloaded to … / started from … with arguments …: (reason)"), which the client already shows as `JobFailed: …`; no client change. The Sockeye crash is n/a: rendering runs as a job, and `JobsBiz.ProcessJobAsync` catches every exception, logs it through NLog and marks the job Failed (`JobsBiz.cs:390-439`), so the process survives. What remains: a browser download or launch failure (`ReportBiz.cs:800`, `:838`, outside the render `try`) reaches the client only as a generic job failure. Catch it in `DoRenderJob` and return a render error that says what failed
|
||||
- [x] **Data-list date filters, small fixes.** Fixed: `>=` and `<` use `ZeroSeconds`; `!=` is `< ZeroSeconds OR > MaxSeconds`; the `DataListField` null check runs first and in all builds, throwing `ArgumentException` naming the column. (An unknown column sent to `/data-list` is in practice rejected earlier by `DataListSqlSelectBuilder`, with its own "DEV ERROR" message; unchanged.) Sockeye 8.1.0 still has the old code. Confirmed: `>=` (`:790-793`) and `<` (`:795-798`) use `MaxSeconds` and should use `ZeroSeconds`; date `NotEqual` (`:806-814`) uses `MaxSeconds` on both sides and should exclude the whole minute (`< ZeroSeconds OR > MaxSeconds`). `DataListField` is dereferenced at `:24` and `:27` before the DEBUG-only null check at `:33`, so **release builds** throw a NullReferenceException on an unknown column, not only DEBUG builds
|
||||
- [x] **Relative date tokens use the server's time zone** **Fixed in case 4697** (boundaries converted to UTC with `clientTimeStamp.Offset`). (`*today*`, `*yesterday*` …). Confirmed: `ClientToday = clientTimeStamp.Date` (`:399-400`) has `Kind=Unspecified`, and `ToUniversalTime()` (`:1060`, `:1065`) converts it with the server's zone. Wrong for any user in a different zone from the server. The fix builds the range from `clientTimeStamp.Offset`, with care for ranges that cross a DST change. **This changes behaviour** and affects the integration tests' `TIME_ZONE_ADJUSTMENT`. Probably better as a follow-up release
|
||||
|
||||
Not port items, listed so they are not forgotten: the three server bugs the integration
|
||||
suite is waiting on (case 4653, case 4654, the contract issue).
|
||||
|
||||
## 4. Test under realistic conditions
|
||||
|
||||
- [ ] **Database role.** Run under a non-superuser role **named like the database**, with a password containing spaces and `@ : / ? # %`. Boot, on-demand backup, scheduled backup, restore. Sockeye ran as `postgres` for years and never exercised either bug in section 3
|
||||
- [ ] **Restore with a dedicated role.** `createdb -O <role>`, then `pg_restore --no-owner --no-privileges --role=<role>`. Without `--role`, a superuser restore leaves every table owned by `postgres`, and the integrity check, which counts through `information_schema`, fails. Compare with the restore instructions in the customer manual (`docs/`) and note any change needed
|
||||
- [ ] **Reports, all advanced reports**, compared with the baseline PDFs. On Windows and on Linux. On Linux also: `ldd` on the downloaded browser binary for missing libraries (install exactly what is reported; several Ubuntu 24.04+ names carry a `t64` suffix); the sandbox under the service account; no orphaned `chrome` processes after a cancelled render
|
||||
- [ ] Integration suite: same result as the baseline
|
||||
- [ ] Licence verification gate: green
|
||||
- [ ] Cypress E2E against a devtest server running the .NET 10 build
|
||||
- [ ] `build-release.bat` produces all five packages; install and start each one: Windows standalone, Windows LAN, Linux server, Linux desktop, subscription Linux
|
||||
- [x] **Database role.** Done on the Ubuntu 24.04 droplet (2026-10-23): role `"AyaNova"`, no superuser, no CREATEDB, password `p @:/?#% x`; boot, on-demand backup (818 entries), 22/22 reports and the integration suite all pass. Scheduled backup uses the same code path as on-demand. Run under a non-superuser role **named like the database**, with a password containing spaces and `@ : / ? # %`. Boot, on-demand backup, scheduled backup, restore. Sockeye ran as `postgres` for years and never exercised either bug in section 3
|
||||
- [x] **Restore with a dedicated role.** Done on the droplet: `createdb --owner=AyaNova`, `pg_restore --no-owner --no-privileges --role=AyaNova`; every table owned by the role, integrity check and licence pass at boot. A plain superuser restore (`dropdb`/`createdb`/`pg_restore` as `postgres`) of an 8.2.4 dump into 8.3.0 also works. Manual compared (2026-10-23): `ops-restore.md` restores as `postgres` into the existing database (`pg_restore -U postgres -c -d AyaNova …`), which is the default setup and matches the tested superuser restore; no change needed for 8.3.0. A customer with a dedicated role would need `--role` (see Deferred). `createdb -O <role>`, then `pg_restore --no-owner --no-privileges --role=<role>`. Without `--role`, a superuser restore leaves every table owned by `postgres`, and the integrity check, which counts through `information_schema`, fails. Compare with the restore instructions in the customer manual (`docs/`) and note any change needed
|
||||
- [x] **Reports, all advanced reports**, compared with the baseline PDFs. Done: Windows (page counts = PuppeteerSharp 17), Ubuntu 24.04 and 26.04 droplets, 22/22 each; John checked the PDFs visually; `ldd` clean on both Ubuntu versions; no orphaned browser processes after a cancelled render. Was: On Windows and on Linux. On Linux also: `ldd` on the downloaded browser binary for missing libraries (install exactly what is reported; several Ubuntu 24.04+ names carry a `t64` suffix); the sandbox under the service account; no orphaned `chrome` processes after a cancelled render
|
||||
- [x] Integration suite: same result as the baseline. Locally on every step; on both droplets with `UserWithActivityShouldNotBeDeleteable` excluded (it deletes SuperUser when SuperUser has no related records, see Deferred)
|
||||
- [x] Licence verification gate: green. 730/730 on 2026-10-23 at `181b1e44` (`dotnet test ayanova.tests`); re-run in the final gate (section 5)
|
||||
- [x] Cypress E2E (`raven-test\e2e`) against a devtest server running the .NET 10 build (`devtestn10` is kept for this until release, John). First set `"expectedServerVersion"` in `raven-test\e2e\cypress.json` to 8.3.0 (it says 8.2.4). **Done 2026-10-26: 27/27** on `devtestn10` with the release candidate build (Electron, from the workstation). Getting there fixed the tests, not AyaNova (`raven-test` `e0b3641`, `ad8fbde`, `2ec1e03`): logins failed at random over the internet because the login page reloads once on a first visit (service worker taking control) and can move the focus back to the username field while it settles; 8.2.4 on devtest failed the same way. `0900` looked for its notification before the server's once-a-minute delivery; it now reloads until it arrives. Also covers the UI half of "exercise LINQ-based queries screen by screen" (section 1)
|
||||
- [x] `build-release.bat` produces all five packages; install and start each one: Windows standalone, Windows LAN, Linux server, Linux desktop, subscription Linux. All five built (2026-10-23, from `4a011f65`+). **Linux done** on droplet `devtestn10` (Ubuntu 24.04): subscription zip as an in-place upgrade of 8.2.4, perpetual server and desktop zips booted fresh (see log). **Windows standalone done** in Windows Sandbox (upgrade 8.2.4 → 8.3.0, uninstall, fresh install; see log); fresh install needed the `standalone.iss` PostgreSQL folder fix. **Windows LAN done** in Windows Sandbox (fresh install following the manual, see log). Re-check both Windows installers from the final release build (they carry the fixes made during this test)
|
||||
|
||||
## 5. Release
|
||||
|
||||
- [ ] **Every host that runs a framework-dependent build needs the .NET 10 runtime** before it gets the release:
|
||||
- Hosted subscription droplets (Ubuntu 24.04). How `aspnetcore-runtime-10.0` is obtained on 24.04 needs checking **[unverified]**. On 26.04 it is in Ubuntu's own archive
|
||||
- Hosted subscription droplets (Ubuntu 24.04). **Confirmed on the test droplet:** `apt-get install -y aspnetcore-runtime-10.0` gets 10.0.13 from Ubuntu's own archive (`noble-updates/main`, `noble-security/main`), no PPA. Installing it replaces `dotnet-host-8.0` with `dotnet-host-10.0` (the shared `dotnet` command); the 8.0 runtime stays installed and unused. Tested upgrade 8.2.4 → 8.3.0 in place: stop service, install runtime, the manual's file swap, start; boots and keeps the licence. 26.04: confirmed on the rebuilt droplet (fresh install with the updated setup script, 2026-10-23); 8.2.4 cannot run there (no .NET 8 in 26.04)
|
||||
- **Subscription server setup script** (`sockeye-client/src/views/biz-subscription-server.vue`, around lines 1350-1423; filled in per server and copied to the clipboard from the "Script" menu, used for every new subscription droplet): `apt-get install -y aspnetcore-runtime-8.0` (line 1377) → the .NET 10 runtime from whichever source the test droplet confirms; remove the `mkdir`/`chmod g+rwx` of `/var/ayanova/Chrome` and `ChromeHeadlessShell` (lines 1396-1397, 1402-1403), the browser now downloads to `/var/ayanova/data/report-browser`. Verify by running the updated script on the test droplet. Part of this port (John, 2026-10-22). Runtime and Chrome-folder change committed in `sockeye-client` `main` (2026-10-23); **do not deploy a Sockeye client built from it before 8.3.0 is released** (the script installs the released zip; 8.2.4 needs .NET 8). 26.04 support and hardening committed in `sockeye-client` `main` (`64d8f62`, 2026-10-23), tested end to end on a fresh 26.04.1 droplet: PostgreSQL via `postgresql-common`'s `apt.postgresql.org.sh` (`apt-key` is gone in newer Ubuntu); `libgdk-pixbuf2.0-0` → `libgdk-pixbuf-2.0-0` (the only library name missing on 26.04); unique random PostgreSQL password and JWT secret per server, written to PostgreSQL, `config.json` and `/root/ayanova-secrets.txt` (600); `config.json` 640 `root:www-data`; John's `52no-phasing` apt setting
|
||||
- Self-hosted Linux customers (server and desktop zips are framework-dependent)
|
||||
- Windows LAN: `lan.iss` prompts for the ASP.NET Core Hosting Bundle. Update its text and link to .NET 10
|
||||
- Windows standalone and the launcher are self-contained: no runtime needed on the customer's machine
|
||||
- [ ] Customer manual (`docs/`): system requirements and upgrade instructions for self-hosted customers
|
||||
- [ ] Build VM: install the .NET 10 SDK; update `devdocs/build/DEV-VM-SETUP.md` and `devdocs/build/BUILD.md`
|
||||
- [ ] Final gate runs on the finished code
|
||||
- [ ] Merge, per repo:
|
||||
- Windows LAN: `lan.iss` prompts for the ASP.NET Core Hosting Bundle. Text and link updated to .NET 10 (2026-10-23); tested in Windows Sandbox (section 4)
|
||||
- Windows standalone and the launcher are self-contained: no runtime needed on the customer's machine. **But they carry the .NET runtime of the build machine's SDK:** SDK 10.0.103 → runtime 10.0.3 (the Sandbox test logged `.Net Version - 10.0.3`; Linux hosts run 10.0.13). Before the final release build, update the workstation's .NET 10 SDK to the newest release so the standalone package ships the current runtime with its security fixes. 8.2.4 did the same with .NET 8.0.13. **Done 2026-10-23:** SDK 10.0.402 installed with winget (`winget upgrade` did not find the Visual Studio-installed 10.0.103); test publish of the standalone server and the launcher bundles runtime 10.0.13. Standing note added to `devdocs/build/BUILD.md`
|
||||
- [x] Customer manual (`docs/`): system requirements and upgrade instructions for self-hosted customers. Report browser move (section 3): manual already updated in the same commit (`ops-config-report-render-browser-path.md`, `ops-install-linux-server.md`, `ops-upgrade-linux-server.md`, `ops-upgrade-linux-desktop.md`). .NET 10 runtime install and upgrade steps (Windows LAN and IIS, Linux server and desktop), Ubuntu 24.04/26.04, backup password note and the 8.3.0 changelog draft: `111d7922` (2026-10-23). Changelog wording to be finalised by John before release. Possible extra line: John notices the UI is much faster than 8.2.4 in local debugging (2026-10-24); add a performance line only if confirmed on `devtestn10` against 8.2.4 on devtest (same droplet type and network distance, e.g. Cypress run times)
|
||||
- [ ] Build machines: the workstation has the .NET 10 SDK (10.0.402, plus 10.0.103 from Visual Studio). The backup laptop needs it before it can build 8.3.0 (`winget install --id Microsoft.DotNet.SDK.10 -e --source winget`; the .NET 8 SDK is no longer needed); tracked with the laptop's other catch-up items in `devdocs/build/BUILD.md`. `devdocs/build/DEV-VM-SETUP.md` and `devdocs/build/BUILD.md` updated (2026-10-23). The build VM is deprecated (John, 2026-10-23): n/a
|
||||
- [x] Final gate runs on the finished code (after the "next release" cases, on the release build from `main`). **Done 2026-10-26 at `c19a3b04`** with SDK 10.0.403 (runtime 10.0.14): licence gate 730/730; integration suite on a fresh trial database 149 tests, 143 passed, 2 failed (the two known: `ContractCrud.CRUD`, `CustomerWithWorkOrders_CannotBeDeleted`; the only two errors in the server log are theirs), 4 skipped; no EF Core warning raised as an error. Release candidate build from the same commit: all five packages 8.3.0, Windows standalone and launcher bundle runtime 10.0.14, Linux zips `http://*:7575` as shipped; installed on `devtestn10` as a subscription upgrade, hardening intact. Final build follows John's changelog edit (docs only)
|
||||
- [x] Merge, per repo. **Done 2026-10-23, without a tag** (John): port merged before the "next release" cases so the port and each case stay separate in history. `raven` `36633011`, `raven-client` `44d91611`, `raven-launcher` `d296298`, `raven-test-integration` `0384bb8`; before each merge `main` had no commits outside `net10-port`, after it `git diff net10-port main` was empty; pushed. From here: cases as their own commits on `main` with their own case numbers; the tag lines below run at release. An urgent 8.2.x fix would branch from `net8-baseline` (= 8.2.4):
|
||||
```powershell
|
||||
git switch net10-port; git status; git push; git fetch origin
|
||||
git switch main; git pull
|
||||
@@ -152,28 +162,173 @@ suite is waiting on (case 4653, case 4654, the contract issue).
|
||||
```
|
||||
`--no-ff` keeps a merge commit, so the whole port can be backed out with `git revert -m 1 <merge>`
|
||||
- [ ] Rebuild the client from `main`, publish, and test **the published artifact itself**
|
||||
- [ ] Rollout: own test droplet first, then customer droplets by time zone, as in the weekly routine
|
||||
- [ ] After uploading, download every installer and package back from ayanova.com and check its version: Windows installers by the .exe's ProductVersion, Linux zips by the `AyaNova.dll` inside. (Found 2026-10-25: the LAN installer on the website was still the 8.2.3 build, same file as downloaded on 2026-10-23, so 8.2.4 was never uploaded for LAN.)
|
||||
- [ ] Rollout: own test droplet first, then customer droplets by time zone, as in the weekly routine. **Subscription droplets are upgraded before the general release** (John, 2026-10-26, standing rule from 8.3.0 on): upload `latest-version.json`, which announces the release to every AyaNova installation, only after the last customer droplet is upgraded. Each droplet also gets the one-time hardening (`harden-830.sh`, case 4727) right before its upgrade
|
||||
- [ ] After release: delete `net10-port` locally and on the remote
|
||||
|
||||
---
|
||||
|
||||
## Release 8.3.0 cases
|
||||
|
||||
John's "next release" cases for AyaNova (exported 2026-10-24). Each is its own set of commits on
|
||||
`main` (`case NNNN - …`). Status: open unless noted. Security cases map to findings in
|
||||
`SECURITY-REVIEW.md` (finding IDs are permanent).
|
||||
|
||||
**Security review** (priority 00)
|
||||
|
||||
| Case | Name | Findings |
|
||||
|---|---|---|
|
||||
| 4710 | 2FA step requires the temp token (raven and sockeye) | F5, F41. **Done**: raven `141bdeb8` (proof script 401 on both bypasses; integration test `TwoFactor` in `8e5eec4`); sockeye `f7c1d7c` (same checks on local Sockeye with a temporary user). Sockeye to be deployed to its new droplet by John; 2FA can be turned back on once each server runs the fix |
|
||||
| 4711 | JWT secret setting removed: the server generates its signing key once into `jwt-signing.key` in the data folder (4698 folded in) | F1. **Done** (placeholder-forged token 401; key survives restart; auth tests pass). Hosted setup script's secret step now does nothing; tidy later in `sockeye-client` |
|
||||
| 4712 | Customer contact route allowlist + ownership checks | F13, F14, F15, F16, F17. In 6 steps: 1 allowlist **done**; 2 work-order ownership **done**; 3 CSR and unit ownership **done**; 4 pick lists for contacts **done**; 5 schedule staff rights **done**; 6 notification subscriptions **done**. All steps done. John clicked through the customer screens as Customer, HeadOffice and CustomerRestricted contacts (2026-10-24): no failures; contact attachments checked by API (own work order list and download work, another customer's 403) |
|
||||
| 4713 | Contact users limited to customer roles | F28. **Done** (validation refuses staff roles on contacts; contacts act with customer roles only, also on download routes; test `ContactRoles`) |
|
||||
| 4715 | Download file names checked (one helper) | F21. **Done** (`FileUtil.FullPathInFolderOrNull` in temp/backup exists checks and both log routes; test `DownloadNames`) |
|
||||
| 4716 | Pick-list search text as SQL parameters | F18, and F43 (unit pick-list variant parsed as a number, found during 4712, test `FetchUnitPickListCustomerVariant`). **Done** (`@plq`/`@pltag` parameters; test `FetchUserPickListAutoCompleteApostrophe` fails on the old code, passes now; 9/9 pick-list tests) |
|
||||
| 4717 | KPI interval allowlist | F26. **Done** (`KPIFetcher.DateTruncUnitOrNull` in the 7 KPIs reading `interval`; test `KpiInterval`) |
|
||||
| 4718 | Report and translation import need create rights | F19, F24. **Done** (contact and Service 403, BizAdmin 202; test `ImportRights`) |
|
||||
| 4719 | Report page network restricted; notification reports not run as SuperUser | F20. **Done**, plan changed with John: 1 report templates changeable only by BizAdmin (`2f616cba`, test `ReportEditRights`); 2 the page holds placeholders for the token and download key, the server adds the real ones only on requests to `ayApiUrl` (interception in every kind of install; test `ReportRenderCredentials`; 22 stock reports unchanged locally; IIS VM user and notification reports); outside addresses not blocked (customer templates may load their own content), notification reports keep their rights (customers write from reports). Visible: templates calling the API by another address need changing (manual and changelog); attachment links printed into reports carry no download key (manual only, John: such links pointed at 127.0.0.1 unless the override was set, so rarely worked) |
|
||||
| 4720 | Restricted roles filtered in report/export data | F27. **Done** (partial fetch filtered like the full one; BizObjectFactory passed UserType.Service for everyone, now the real type; test `RestrictedReportData`) |
|
||||
| 4721 | Client escaping of record text | F33. **Done** (raven-client: `escapeHtml` helper for the warranty summary; address dialog as text; search excerpt escaped with the terms highlighted as plain text; dialog messages through DOMPurify. The excerpt is not encoded on the server as well: the client escaping covers it. John checked search, warranty info with `<b>` in a vendor name, the address dialog and confirmation dialogs) |
|
||||
| 4722 | DOMPurify and Vuetify upgrades (raven-client) | F34. **Done** (raven-client `44411ec1`: DOMPurify 2.5.9, Vuetify 2.7.2, builds on Node 12.22.9; Vuetify 2.7's calendar `eventSummary()` returns an element, so the six calendar event templates render it with a new `gz-vnodes` component instead of `v-html`. John checked wiki, schedules (drag, stretch, lock icons), date pickers, lists, dialogs and the reminder and review widgets. John also checked the scheduled work order widget as a Tech user. Still to do: Cypress on `devtestn10` at the final gate) |
|
||||
| 4723 | Direct SMTP and direct notification: staff only, record rights | F3, F4. **Done**, plan revised with John: staff keep sending to any address (stock report template and a customer script use it); contacts refused in both actions, an address from a record needs read rights on it, direct notifications refused to subcontractors. Test `DirectSend` (the subcontractor case returned 204 on the old code). No changelog line: nothing that works changes |
|
||||
| 4724 | SMTP: certificate checked, password never returned | F30, F31. **Done** (certificate checked, opt-out config setting `AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE` with manual page; password masked as `********` and kept on save; test `SmtpPasswordHidden`; self-signed listener proof; changelog lines for both) |
|
||||
| 4725 | Sessions: password change ends sessions, exact-path token exemption, default SuperUser refused when licensed | F7, F8, F9. **Done** (sessions end on reset, administrator password change and login disabled; exact route exemption, inactive users refused; default-password SuperUser limited to changing it on a purchased licence, option A; test `Sessions`; John's click-through with a temporary licence override; manual and changelog) |
|
||||
| 4726 | Login throttling/lockout (raven and sockeye) | F6. **Done**; the Sockeye half (`6d6c74f`), like 4710's, takes effect when Sockeye is deployed to its droplet (10 failures per login or 50 per address in 15 minutes, then 429 for the rest of the window; same in Sockeye, whose login is public; test `LoginLockout`; per-address limit proved with a forwarded documentation address) |
|
||||
| 4727 | Low severity hardening | F11, F12, F22, F23, F25, F29, F35, F36, F38; the AyaNova half of F39 (DB ID not shown anonymously). In steps: 1 logo (F23) **done**; 2 API root page (F11, F39) **done**; 3 `server-info` (F29) **done**; 4 template export (F22) **done**, job owner check accepted; 5 `OverrideUserId` and export download (F25) **done**; 6 safe headers (F35) **done**; 7 remove `dist/docker` (F38) **done**. All steps done; `sockeye-client` setup script: bind `127.0.0.1`, the `chmod`, the systemd options (F36, F38) **done** (`9f300a6`, tested on `devtestn10` 2026-10-26; existing droplets get `harden-830.sh` at the rollout); the Linux server install and upgrade manual pages updated to match (permissions, service settings, `127.0.0.1` behind NGINX, `curl -fO`). **Case done.** F10 moved to case 4736, after 8.3.0 (John, 2026-10-26) |
|
||||
| 4681 | Sockeye: licence/trial request hardening (tagged sockeye, rockfish) | F40. **Done** (sockeye `cd3ebed`: rate limits, email check, cryptographic codes; rvf unchanged) |
|
||||
| 4732 | Trusted reverse proxies setting (from the Sockeye migration runbook, Appendix A) | F6 follow-up. **Done** (`AYANOVA_TRUSTED_PROXIES`: addresses or CIDR networks, chain followed, invalid entries warned at boot; one log warning per start when `X-Forwarded-For` comes from an untrusted proxy; manual page and changelog. Proved by hand through the LAN address: not set, logins recorded as the proxy and the warning logged; set, the forwarded client address used, a forged extra entry ignored, a two-proxy chain followed; localhost unchanged. Not needed in Sockeye: its nginx connects on loopback) |
|
||||
|
||||
**From the port and earlier triage**
|
||||
|
||||
| Case | Name | Priority |
|
||||
|---|---|---|
|
||||
| 4735 | Roles audit: every documented role right against `BizRoles.cs` (found in 4719, see Deferred) | 00. **Done** (`27b56db6`, `7959ad9c`). Audit: 60 manual role lists and 19 prose sections against 89 types (script); besides report templates (4719) 15 differences, one a false positive, decided with John 2026-10-25. **Code** (commit 1): Tech restricted reads tax codes, projects, unit models, meter readings; BizAdmin and restricted read server metrics; Accounting no longer changes meter readings (both meter reading routes in `UnitController` checked Unit rights, now meter reading rights); no Subcontractor roles on memos; integrations: data operations stay open to all full staff roles (third-party apps may sign in as any role, QBI as Accounting), switching Active and deleting are BizAdmin only. Test `RoleRights` (6; before: the 5 changed rights behaved the old way; Tech still enters meter readings). **Manual** (commit 2): travel rates role names (the page named non-existent "Travel" roles), customer service requests created by customer contacts. Work orders: Inventory was a false positive of the audit script (it read the page's part cost visibility list), the role lists are right. Subcontractors still cannot enter meter readings (never could; John: keep). Lookups readable by every role are worded in prose, no change |
|
||||
| 4692 | SuperUser can be deleted | 00. **Done** (`UserBiz.ValidateCanDelete` refuses user 1 with 2004 before any other check; the integration test `UserWithActivityShouldNotBeDeleteable`, which deleted SuperUser on a database where it had no related records and so was excluded from every run, is now `SuperUserShouldNotBeDeleteable` and runs with the rest: full suite = baseline, SuperUser still logs in afterwards; changelog) |
|
||||
| 4693 | FormFieldReference start-up race | 00. **Done** (both lazily built lists, `FormFieldReferenceList` and `FormFieldKeys`, are built in a local and published when complete, fields volatile; three restarts each followed at once by the full suite: baseline every time, no "is not valid" in the log) |
|
||||
| 4654 | General notification delivers at most once per 12 hours (circuit breaker) | 00. **Done** (direct messages are general notifications with no object, so the 12-hour repeat check in `CoreJobNotify` (case 3917, keyed on subscription and object) dropped every message to a user after the first; for general notifications the check now applies only to the job's own delivery error messages (`DELIVERY_ERROR_NAME`, "Error"); integration test `DirectMessage_SendAndDelete` un-skipped and now sends two messages: before, only the first arrived; after, both; full suite = baseline; changelog) |
|
||||
| 4694 | Standalone launcher not shutting down PostgreSQL (raven-launcher) | 1. **Done** (launcher: `pg_ctl stop -m fast -w` when the server exits, replacing the `StandardInput.Close()` that threw, and on window close (SIGHUP/CTRL_CLOSE); `standalone.iss`: `PrepareToInstall` and uninstall stop a running embedded PostgreSQL with the installed `pg_ctl`. Sandbox: 7 `postgres` processes left running as by 8.2.4, then the upgrade showed no "files in use" page and stopped them; Shut down server and closing the window each left no `postgres`. Note: closing the window or Ctrl+C already ended PostgreSQL in 8.2.4 (same console); only the in-app shutdown left it) |
|
||||
| 4695 | Shipping the Visual C++ runtime DLLs with the standalone install | 1. **Done** (`msvcp140.dll`, `vcruntime140.dll`, `vcruntime140_1.dll` 14.44.35211.0, Microsoft-signed, added to `dist/win-x64/postgres-standalone/bin`, app-local; BUILD.md notes they must be replaced by hand. Sandbox: fresh Windows with no runtime, install started PostgreSQL, trial, report 1.6 s) |
|
||||
| 4696 | JWT secret missing from the standalone config.json | 1. **Done, resolved by 4711, no change** (sandbox 2026-10-25: 8.2.4 warned at every boot that AYANOVA_JWT_SECRET is missing; after the upgrade to 8.3.0 the first boot generated `C:\ProgramDatayanova\jwt-signing.key`, the next boot reused it with no warning, and a fresh 8.3.0 install did the same) |
|
||||
| 4697 | Relative date filters using the server's time zone | 1. **Done** (`BuildSQLBetweenTwoTokenDerivedDates` converts the user's local boundaries to UTC with `clientTimeStamp.Offset`, was `ToUniversalTime()` with the server's zone; the dashboard KPIs use the same builder, so their ranges are fixed too. Test `WorkOrderList_TodayIsTheUsersToday`: a work order at 00:30 today for a +10:00 user, before missing from *today*, after included; full suite = baseline. The manual already promised the user's time zone (`ay-start-data-types.md`, `ay-start-form-data-tables.md`), no manual change; changelog) |
|
||||
| 4699 | Linux desktop data-path note in the manual | 1. **Done** (`ops-install-linux-desktop.md` step 4: no `sudo mkdir ~/.ayanova-data`; AyaNova creates the folder, `~` is root's home under sudo, full path for another location) |
|
||||
| 4700 | Restore note for customers with a dedicated database role | 1. **Done** (`ops-restore.md`: `--no-owner --no-privileges --role=<user>`) |
|
||||
| 4703 | Report JS console error logging | 1. **Done** (error messages were already logged; what was lost: object arguments shown as `JSHandle@object`, no stacks. Now each `ERROR:` line is logged at once as before, then each object or Error argument is fetched from the page in the background and added as a `detail for "<message start>":` line (object JSON cut at 1000 characters; error stack without Puppeteer frames, paths shortened to file names); a failing render waits up to 2 s for pending details; page log appends locked (console thread, relay). Better than Sockeye's version, which loses the whole line when fetching details fails. Test `ConsoleErrorDetailsAreLogged`; 22 stock reports unchanged; manual (report designer, Script errors) and changelog) |
|
||||
| 4705 | License.DoFetchAsync never tries the fifth licence server | 1. **Done** (rethrow after the last server, `x == 4`; the trial request (`rvr`, rockfish only) now uses the same fallback loop, John's decision. Sandbox with rockfish, io, europa and callisto pointed at 127.0.0.1: the old build failed the trial request with a connection error, the new build reached sockeye.ayanova.com. No changelog: only noticeable if the licence server names fail) |
|
||||
| 4706 | Remove the unused RunProgram.Run | 1. **Done** (`Run` and its `RunWindows`/`RunLinuxShell`/`RunOSX` removed, no callers; `RunNoShell` stays. Backup through the API wrote the database dump and attachments zip; full suite = baseline) |
|
||||
| 4707 | Stray cypress.json in raven-client | 1. **Done** (raven-client `f2f10b81`: removed; from 2020, pointed at `tests/e2e`, which moved to `raven-test` in 2022 (`e0be8a7c`); Cypress is not a client dependency) |
|
||||
| 4687 | Generator polls every second (raven and sockeye) | 1. **Closed, no change** (John, 2026-10-26). Measured on an idle local server over 60 s: 4.5 database transactions per second (about 315 rows read per second, mostly the ready-jobs checks), AyaNova CPU 0.14 s per minute (0.2 % of one core). Wake-on-demand (5 s sleep, woken by `JobsBiz.AddJobAsync`) was considered and rejected: small gain, timing change in the loop that runs every background job |
|
||||
| 4682 | Time-only picker keeps the old date's DST offset | 1. **Done** (John: the backup time is the server's clock time, the schedule's first hour the user's. Backup: the time is stored as the UTC moment on the day it was set and was run at that UTC hour daily, so it moved an hour on the server's clock at each DST change (John saw backups start at an unexpected time); `ServerGlobalOpsSettingsCache.SetNextBackup` now recovers the clock time with the stored date's offset and runs it on the server's local days (`BackupTimeOnLocalDay`; a time skipped when clocks go forward runs an hour later). Proof (temporary log line, removed): 11:30 PM set in January (07:30Z) gave 00:30 PDT before, 23:30 PDT after. Schedule: `home-schedule.vue` already keeps `firstTime` as local "HH:MM", no change. No migration. Manual (backup time) and changelog. Sockeye has the same code: same fix in sockeye, takes effect when Sockeye is next deployed (matters for the droplet cron job that copies backups if its tzdata still has DST for America/Vancouver). Note: this workstation's Windows zone is "Pacific Time (US & Canada)", which still falls back on 2026-11-01 although BC abolished DST; AyaNova follows the OS zone) |
|
||||
| 4661 | Docs: password guidance | 1. **Done** (`home-password.md`, `ay-start-login.md`: no lyrics/quotes advice; password manager or six dice-chosen EFF words; no length or character rules) |
|
||||
| 4636 | Docs: turn off 2FA before v7 to v8 migration | 1. **Done** (`ops-import-v7.md`: 2FA off on the v8 SuperUser for the migration; also, from 4725, change the default SuperUser password first on a purchased license) |
|
||||
| 4634 | Docs: custom checkbox shows nothing for older records | 1. **Done** (`ay-customize.md`, "Checkbox custom fields on existing records") |
|
||||
| 4633 | Docs: pg_dump path quoting on Windows (likely fixed by the port; confirm) | 1. **Done, confirmed fixed by the port** (backup with `AYANOVA_BACKUP_PG_DUMP_PATH` in a folder path with spaces: dump written, no errors; no manual workaround existed; changelog backup line extended) |
|
||||
| 4632 | AYANOVA_REPORT_RENDER_API_URL_OVERRIDE breaks customer notification reports (do before or with 4719) | 1. **Done.** Cause, reproduced on a Windows Server 2025 Hyper-V VM (IIS, https-only site with a host name, as for internet access): AyaNova runs inside IIS (in-process), so nothing listens on `AYANOVA_USE_URLS`; user reports called `http://127.0.0.1:<port of the request>` (443 over https: connection reset), notification reports `127.0.0.1:7575` (refused); the override only fixed user reports, and only where the server reaches itself by its public name. Fix: under IIS the report page's requests under the API URL are intercepted (PuppeteerSharp request interception) and made by the server (`util/ReportApiRelay.cs`) to the local address and port a user's request arrived on, with its host name and scheme (IIS binding and certificate match, no DNS/router hairpin); notification reports use the latest signed-in request, else the override; the page is opened at the API origin so its calls are same-origin; outside IIS nothing changes. The notification token check no longer requires host `127.0.0.1`, only a request from this machine. Proof: VM before (both fail) and after (user report 1.6 s and notification email with PDF, https-only with and without the override); locally 22 stock reports identical with the relay forced on (temporary switch, removed); integration suite = baseline. Manual: IIS install page and override page; changelog |
|
||||
| 4628 | Deleting a work order tied to a CSR gives a raw server error | 1. **Done** (the request's `workorderitemid` foreign key blocked deleting the item, a 500 from the database; John chose: delete, unlink the request and set it back to Open, no notification. `WorkOrderBiz.ItemDeleteAsync` does that in the delete's transaction and logs a Modified event on the request; covers deleting the item alone too. Test `WorkOrderFromServiceRequest`: before 500, after 204 and the request Open and unlinked; full suite = baseline. Manual (service requests) and changelog) |
|
||||
| 4623 | Docs: migration, what is erased and what is kept | 1. **Done** (`ops-import-v7.md`: Erased and Kept lists from `DbUtil.EmptyBizDataFromDatabaseForSeedingOrImportingAsync`; tax codes erased by the migration) |
|
||||
| 4602 | Logo upload UI: hide Upload and Delete until usable | 1. **Done** (raven-client: logo form shows file choosers only with change rights, Upload once a file is chosen, Delete only for an existing logo; John added the attachment control's Upload, now shown once files are selected. Import, report template and translation uploads already did this. Checked by John as BizAdmin and BizAdmin restricted. No changelog) |
|
||||
| 4579 | Windows installer modify rights: only the folders AyaNova writes to, only for the account it runs as (includes F2) | 00. **Done** (both installers: no `users-modify` on `{app}` or `local-postgres`; `icacls "{app}" /remove:g *S-1-5-32-545 /T /C /Q` after install removes the Users grant on upgrades too; data folder keeps `users-modify` and `config.json` stays readable by local users, accepted by John and documented in the single-user and LAN install pages. Sandbox: single-user fresh and upgrade over 8.2.4, LAN upgrade; Users modify entries under `{app}` 4632/1281 before, 0 after; trial, login, attachment, backup, report render after upgrade. Found on the way: the downloaded "8.2.4" LAN installer installs server 8.2.3, which fails its integrity check on a new PostgreSQL 18 database (E1030); gone once the 8.3.0 LAN installer is published) |
|
||||
| 4690 | .NET 10 port umbrella case | 1 |
|
||||
|
||||
Sockeye cases (exported 2026-10-24): 4710 (also in the table above) and 4681. 4681 predates the review: a trial request with a made-up address made Sockeye send its verification email, which bounced to support@ (Fastmail notice, 2026-10-16).
|
||||
|
||||
Moved out of 8.3.0 by John (2026-10-24): 4665 (health route checks the database) goes to the release after.
|
||||
|
||||
Later case to create (John, 2026-10-24): **repeatable licence fetch and server fingerprint**. AyaNova skips installing a fetched key identical to the installed one and adds to its rvf request a flag (it accepts repeat replies) plus a fingerprint: version, build (perpetual or subscription), platform, installation type (standalone, Windows LAN/IIS, Linux server, Linux desktop); Sockeye returns already-fetched keys only to flagged requests (older servers keep the one-shot reply) and stores the last fingerprint per DB ID, so John can see which versions and installation types are in use (e.g. whether standalone can be deprecated). Takes the Sockeye half of F39. Mention in the manual that the server sends this information.
|
||||
|
||||
Later, not 8.3.0: F42 (move the licence signing key out of Sockeye source) and F32 (rotate the
|
||||
FastMail password after the old Sockeye server is retired), John's cases.
|
||||
|
||||
## Open questions
|
||||
|
||||
- Release version number (Sockeye's port was released as 8.1.0)
|
||||
- `raven-launcher` (.NET 8, self-contained): move to .NET 10 in the same release?
|
||||
- `raven-test-integration`: move to `net10.0` (separate commit) or leave on `net8.0`? It only talks HTTP to the server
|
||||
- Does the client need a rebuild in this pass, or can its current output be carried across?
|
||||
- Release version number (Sockeye's port was released as 8.1.0) Answer: 8.3.0 will be the version
|
||||
- `raven-launcher` (.NET 8, self-contained): move to .NET 10 in the same release? Answer: Yes. It has no package references, so this is the target framework only
|
||||
- `raven-test-integration`: move to `net10.0` (separate commit) or leave on `net8.0`? It only talks HTTP to the server Answer: Move to net10.0 for consistency
|
||||
- Does the client need a rebuild in this pass, or can its current output be carried across? Answer: yes it will have minor changes as well as version number to 8.3.0
|
||||
- Section 3: which fixes go in this branch, which in a follow-up
|
||||
- How the .NET 10 runtime gets onto Ubuntu 24.04 hosts
|
||||
- How the .NET 10 runtime gets onto Ubuntu 24.04 hosts. Today's setup script uses Ubuntu's own archive (`aspnetcore-runtime-8.0`); to be settled on the test droplet
|
||||
- MiniProfiler (section 1): keep it and upgrade to 4.5.x (check that `MiniProfiler.EntityFrameworkCore` supports EF Core 10), or remove it in its own commit? Answer: remove it, never used
|
||||
|
||||
## Deferred
|
||||
|
||||
Found during the port, out of scope. One line each.
|
||||
|
||||
-
|
||||
- **Fixed in case 4762.** `NotifyController.SendNotifySmtpDirectMessage`, User branch: the missing-address and inactive messages say "Vendor" instead of "User" (found in case 4723)
|
||||
- SMTP connection security mapping (`Mailer.cs`): "StartTls" connects with `useSsl: true`, which MailKit treats as TLS from the start (port 465), not STARTTLS (587); "None" uses STARTTLS when offered, which can be stripped. Correcting it could break customers using "StartTls" on port 465 (found in case 4724)
|
||||
- Integration test `DownloadNames` logs in again as the shared `Sales` user to get a download key, which replaces the token the other tests use for Sales while running in parallel (one session per user). Tests that need a download key should create their own user, as `TemplateExportRights` does (found in case 4727). Also fragile under parallel runs: `DataListOperations.WorkOrderList_PaginationOffsetAndLimitWork` fetches two pages of work orders and checks they do not overlap, while `WorkOrderCrud` creates and deletes work orders (failed once in case 4727)
|
||||
- From F6 (case 4726), split off as separate changes: a password strength rule (not empty is the only rule today; seeded trial users use their login as password) is a new, visible feature; `limit_req` on `/api/v8/auth` in the hosted nginx configuration belongs in the `sockeye-client` setup script and affects every hosted droplet. Both are second layers behind the login lockout
|
||||
- Race in the notification system: a record saved while a matching notification subscription is being deleted fails with 500 (`anotifyevent_notifysubscriptionid_fkey` violation), and the user's save fails with it. Seen once in the integration suite (`CustomerCrud.CRUD`, run in parallel with subscription tests) during case 4724
|
||||
- **Case 4731 (John, priority 1, after 8.3.0):** a global notification setting "allow email to any address from the API and report scripts" for `notify/direct-smtp`; off for new installs, on for upgraded databases; when off, only a record's saved address. Follows from F3 / case 4723
|
||||
- **Fixed in case 4705.** `License.DoFetchAsync` (`License.cs:629-661`) rethrows at `x == 3`, so the fifth server (Sockeye) is never tried, and it can return `null` to a caller that dereferences it
|
||||
- **Removed in case 4707.** Stray `raven-client\ayanova\cypress.json`: the Cypress suite lives in `raven-test\e2e`
|
||||
- **Race, affects production after every restart:** `FormFieldReference.FormFieldReferenceList` (`biz/FormFieldReference.cs:57-59`) builds a static dictionary lazily with no lock, assigning it before filling it. A concurrent first request sees it half-built and throws `FormFieldReferenceList: <type> is not valid`. Seen twice on .NET 10, each time on the first form-field use after boot: `VendorCrud.CRUD`, then `FormCustom.FormCustomUpdate`. Fix: build the dictionary fully before publishing it (`Lazy<T>` or a static initialiser). Pre-existing, not a port change. John's decision: its own case, fixed with the other cases going into this release, not under the port. **Fixed in case 4693 (2026-10-24)**
|
||||
- **Fixed in case 4694.** `raven-launcher` `Program.cs:98` calls `PGProcess.StandardInput.Close()` but standard input was never redirected, so it throws `InvalidOperationException` at shutdown: the "Postgres shut down" step never runs and the launcher exits with an error when the server stops. Postgres was started detached by `pg_ctl start`, so it probably stays running. Pre-existing; candidate for its own case. **Confirmed in the Sandbox test (8.2.4 and 8.3.0):** after "Shut down server" the launcher crashes with exactly this exception and 7 `postgres` processes stay running. Every standalone upgrade then stops at Setup's "files in use" page listing PostgreSQL Server, and "automatically close the applications" fails ("Setup was unable to automatically close all applications"): the customer must end the processes or restart Windows first. One-line fix (stop PostgreSQL with `pg_ctl stop` instead of writing to its standard input). **John, 2026-10-23: own case, fixed with the "next release" cases before 8.3.0**
|
||||
- **Fixed in case 4695.** **Standalone install needs the Visual C++ runtime (pre-existing, same in 8.2.4; John, 2026-10-23: own case, fixed with the "next release" cases before 8.3.0):** the bundled PostgreSQL (`local-postgres\bin\pg_ctl.exe`, `postgres.exe`) needs `VCRUNTIME140.dll`, which neither the installer nor `local-postgres` provides. A Windows without it (a new PC, Windows Sandbox) shows "pg_ctl.exe - System Error: VCRUNTIME140.dll was not found" and AyaNova never starts. Confirmed fix: copying `vcruntime140.dll`, `vcruntime140_1.dll` and `msvcp140.dll` (x64, 14.44) into `local-postgres\bin` made PostgreSQL start. Ship those three in `dist\win-x64\postgres-standalone\bin` (app-local, allowed by Microsoft's redistribution terms) rather than running `vc_redist.x64.exe` (it crashed Windows Sandbox mid-install)
|
||||
- **Resolved by case 4711 (confirmed in case 4696).** Standalone `config.json` (launcher template) has no `AYANOVA_JWT_SECRET`, so every boot logs a warning and generates a random one (logins are lost on restart). Pre-existing; noted from the Sandbox test logs. **John, 2026-10-23: own case, fixed with the "next release" cases before 8.3.0**
|
||||
- **Done in case 4700.** Restore manual (`ops-restore.md`) covers only the default `postgres` setup. A customer running AyaNova under a dedicated, non-superuser role who restores as `postgres` without `--role` ends up with tables owned by `postgres` and a failed integrity check at boot (found in the section 4 role test). Add a short note: `pg_restore --no-owner --no-privileges --role=<role>`. Pre-existing; after the release
|
||||
- **Security review F1, fixed in 8.3.0:** the LAN/IIS (`dist\assets\lan-install-config.json`) and Linux server (`dist\assets\linux-server\config.json`) templates ship `AYANOVA_JWT_SECRET` set to a placeholder that is printed in the manual. The manual tells users to change it (optional on Linux), but an install that keeps it signs logins with a publicly known secret. Consider generating a random secret at install time or refusing to boot with the placeholder. Pre-existing
|
||||
- Standalone and LAN installers share the same Inno Setup `AppId` (`{B2BB79DB-0690-497C-B92B-955F7B468429}`) although they are different products (`AyaNova` vs `AyaNova server`): installing one over the other would be treated as the same application. Pre-existing; not tested. Both also install to the same folder (`{autopf}\ayanova`) and use port 7575, so the two flavours could never sit side by side on one machine anyway; the shared `AppId` only changes what happens when one flavour is installed **over** the other (e.g. an evaluator moving from standalone to LAN on the same PC): Setup treats it as an upgrade of the same program. Changing either `AppId` now would make that flavour's next installer miss existing installs (second Add/Remove entry over the same folder). Standalone is thought to be used almost only for evaluation (John). **John, 2026-10-23: keep both `AppId`s as they are**; the Windows install pages now say only one type per computer, uninstall the other first (backup and restore to switch)
|
||||
- Sockeye 8.1.0 has the NLog 6 archive naming problem fixed here for AyaNova: archives are `log-sockeye_NN.txt`, old `-0` to `-8` archives are never deleted, and its log viewer still sorts by name (it also filters on `ayanova.txt`, not `sockeye.txt`, so the current file is listed twice). Same fix applies: `ArchiveSuffixFormat = "-{0}"` and the date sort
|
||||
- **Security review: customer contact isolation (John, 2026-10-23: urgent, fixed with the "next release" cases before 8.3.0; case numbers to come).** Findings F13 to F17 in `devdocs/modernization/SECURITY-REVIEW.md`, confirmed on the local server as a seeded customer contact: any customer's work order by id (and, by code, its customer report and attachments); other customers' service requests; the full customer list with phones and emails through pick lists; the whole service schedule; notification subscriptions created for another user. Hosted and self-hosted perpetual installations are equally important. Fix direction in the review: block customer-type users by default except the routes the customer UI uses, then add a "belongs to my customer" check to each allowed route that takes an id. F5 (2FA without password), F1 (placeholder JWT secret) and F18 (pick-list search text in SQL) are fixed in 8.3.0 too (John, 2026-10-23). **8.3.0 is not released until every security review finding is fixed or explicitly accepted by John** ("all holes must be plugged"); the review's remaining passes therefore come before the release. Front-end exception (John, 2026-10-23): DOMPurify to 2.5.x and Vuetify to the latest 2.x (2.7.2, or 2.6.16 if 2.7 does not build on Node 12) for security finding F34, each proven by a full client build and Cypress
|
||||
- **Fixed in case 4692.** **SuperUser can be deleted (serious, pre-existing, own case; John's decision 2026-10-23: fixed with the other "next release" cases before 8.3.0 ships, not under the port):** `UserBiz.ValidateCanDelete` (`biz/UserBiz.cs:1055`) only refuses a delete when the user has related records; nothing protects user id 1, although updates to it are blocked (`UserBiz.cs:946`). On the droplet, whose sample data left SuperUser with no related records, the integration test `UserWithActivityShouldNotBeDeleteable` (`DELETE User/1`) succeeded and removed the account; `AYANOVA_SET_SUPERUSER_PW` cannot bring it back (it only resets an existing user 1). Same code in 8.2.4; likely also why `superuser` could not log in on the reused local database at the first baseline attempt. Any user with delete rights on Users can do this
|
||||
- **Removed in case 4706.** `RunProgram.Run` (and its `RunWindows`/`RunLinuxShell`/`RunOSX` helpers) has no callers since the backup moved to `RunNoShell`. It runs through `cmd.exe /C` or `/bin/sh -c` and logs its full command line on error: delete it rather than reuse it
|
||||
- **Own case after the port (John): automated provisioning from Sockeye.** Goal: from a trial subscription server request, one menu item in Sockeye (after John's manual approval) provisions the whole server through the DigitalOcean API, ready for login: droplet, DNS, firewall, setup, licence. Replaces today's manual steps (header comments of the setup script), which are slow and error-prone. Not part of this port
|
||||
- **Done in case 4699.** Linux desktop manual (`ops-install-linux-desktop.md`), pre-existing, same in 8.2.4: `sudo mkdir ~/.ayanova-data` creates the folder in the user's home, but `sudo ./AyaNova` runs as root, so `AYANOVA_DATA_PATH` `~/.ayanova-data` resolves to `/root/.ayanova-data` and the user's folder is never used (data, logs and backups end up under `/root`). Found in the 8.3.0 desktop zip test. Fix the manual (absolute data path, or not running as root) after the release
|
||||
- **Own case after the port (John):** the subscription server setup script is hard-coded in `biz-subscription-server.vue`, so every change needs a new Sockeye client build. Make it editable without a rebuild
|
||||
- **Done in case 4703.** **Improvement, own case after the port (John to create):** bring back logging of JavaScript error details from report page console errors, as Sockeye's port did. AyaNova logs only `ERROR: {message text}`; the per-argument detail (`arg instanceof Error ? arg.message : arg`) is commented out at `biz/ReportBiz.cs:873-876` since case 4586, because `IJSHandle.ExecutionContext` was removed. Sockeye 8.1.0 (`server/biz/ReportBiz.cs:575`) evaluates on the handle instead: `args.Message.Args.Select(arg => arg.EvaluateFunctionAsync("(arg) => arg instanceof Error ? arg.message : arg"))`, which also needs the console handler to be `async` again
|
||||
- **Fixed in case 4652** (work orders, quotes and PMs now leave the missing record out; the other object types already did). Pre-existing, same in 8.2.4 (found in the 4579 sandbox test, John: Deferred): rendering a report for a record that does not exist (e.g. work order ids 1 and 2 in a trial without sample data) fails the job with a generic `ArgumentNullException` ("Value cannot be null. (Parameter 'o')") from `JObject.FromObject` in `WorkOrderBiz.GetReportData` (`WorkOrderBiz.cs:1030`), instead of skipping the missing record or saying it is not found. Other object types' `GetReportData` likely do the same
|
||||
- **Roles audit, case 4735 (John, 2026-10-25, this release; found in 4719):** check every object's rights as documented in the manual (each page's "Authorization Roles required" section and `ay-biz-admin-roles.md`) against `biz/BizRoles.cs`. Report templates were changeable by Business administration - restricted although the roles page uses exactly that case as its example of a view-only restricted role (fixed in 4719). If that slipped through there could be others; for each mismatch decide whether the code or the page is wrong
|
||||
- **Fixed in case 4739** (the fallback is now the browser's own offset as an `Etc/GMT` zone in whole hours; Chrome and Edge 156 already name the BC zone `America/Vancouver`, and Chrome 155, which does not, also uses offset -8 for it, so a BC user on such a browser is one hour off instead of three: the time zone override remains the fix for them). Found 2026-10-27 (John: later case, no BC customers): when the browser cannot name its time zone, the client uses `America/New_York` (`raven-client` `api/locale.js` `getResolvedTimeZoneName`; Sockeye's client the same), so every date and time is shown, entered and sent to reports in New York time. Windows' new "British Columbia Standard Time" zone (BC permanent UTC−7 from November 2026) does this: Chrome returns `undefined` for it, although it knows `America/Vancouver`'s new rules. Seen on John's PC as backup times three hours late on devtest and Sockeye (the servers were right). Workaround: the time zone override in user settings. Fix: fall back to the browser's current UTC offset (fixed-offset zone) instead of New York
|
||||
- Found in the 8.3.0 Cypress runs (2026-10-26; pre-existing, 8.2.4 too): on a first visit over a slow connection the login page can move the keyboard focus back to the username field while it settles (the field has `v-focus` and is re-inserted when the page redraws), so part of a password typed early lands in the username field in plain view; and the service worker reloads the page once when it takes control (`App.vue`, `controllerchange`), clearing both fields. Both within the first seconds of a first visit
|
||||
- From case 4697 (John: Deferred): (1) a relative date range that crosses a daylight saving change can be an hour off at its far end, because the client sends only its current offset with list requests; exact would need the client to send its IANA time zone name too (front-end change). (2) Dashboard widgets group by day, week or month with `date_trunc` on the stored UTC times, so for a user far from UTC an item near midnight lands in the neighbouring bar; same need for the time zone name
|
||||
- From case 4727 (John: later hardening case, 2026-10-26): AyaNova runs as `www-data` on droplets and in the Linux manual (as in Microsoft's ASP.NET Core with NGINX guide), the same account as NGINX's workers, so NGINX can read `config.json` (database password) and write `data`. A dedicated `ayanova` system account isolates them: service `User=`, ownership, the setup script and the manual, plus a migration for existing droplets and self-hosted installs
|
||||
- Found in the case 4727 droplet test (2026-10-26; John: later case, early in a future release so every other test exercises it): stopping the service sometimes logs `ObjectDisposedException` from `JobsBiz.ProcessJobsAsync`, and the server-problem notification it tries to raise fails the same way, so nothing is sent. Pre-existing: 8.2.4 on devtest logged it at 2 of its last 40 stops (2026-09-10, 2026-10-01). Cause: `GeneratorService.StopAsync` (`generator/Generate.cs`) is overridden to return at once without calling `base.StopAsync`, so the host neither signals the job loop nor waits for it, and disposes its services while a pass may still be running; `Task.Delay(1000)` also ignores the stopping token. Fix: remove the override (or call the base) and pass the token to `Task.Delay`; test stopping during a report render and a backup, since shutdown would then wait for the running pass
|
||||
- From case 4755 (tidy-up, no behaviour change): `SetNextBackup` no longer reads `NextBackup`'s previous value, so the startup line `NextBackup = FileUtil.MostRecentAutomatedBackupFileDate()` (`util/ServerGlobalOpsSettingsCache.cs:50`), the reset to `DateTime.MinValue` in `GlobalOpsBackupSettingsBiz` and `FileUtil.MostRecentAutomatedBackupFileDate` itself are dead. Same in Sockeye
|
||||
- Found in case 4755's integration run (2026-10-31): a 500 response produced by the exception handler (e.g. `/` on a debug server whose `wwwroot` has no client) has none of the three safe headers from case 4727 (F35), because the handler clears headers set before it. Integration test `ApiRoute.SafeHeadersOnEveryResponse` fails for that reason on a debug server without the client in `wwwroot`; it passes on a release build
|
||||
- **Fixed in case 4761.** Pre-existing (found in the 4632 IIS test, John: Deferred): emailed report attachments have the content type `application/pdf/pdf`, because `Mailer.cs:51` builds `new MimePart("application/pdf", "pdf")` (media type and subtype) instead of `new MimePart("application", "pdf")`. Mail programs still open the file by its `.pdf` name
|
||||
- Found in case 4653 (pre-existing): `CustomerBiz.ValidateCanDeleteAsync` refuses to delete a customer that has contacts (users), yet `DeleteAsync` then has code to delete the customer's contacts, which can never run. Its comment says contacts were meant to be deleted with the customer, and that the check is there "for now". Decide which behaviour is wanted; same in Sockeye
|
||||
- Found in case 4739: reports rendered by the server for customer notifications use the subscription's time zone, and when it is empty `resource/rpt/ay-report.js` falls back to `America/Winnipeg` (date, time and date-time helpers), an arbitrary zone like the client's old New York default. Decide on a better default (the server's own zone?)
|
||||
- Found in case 4683 (pre-existing, raven-client and sockeye-client): the **Today** and **Now** buttons of the date, date-time and time pickers (`components/date-time-control.vue`, `time-control.vue`) take the date and time from the browser's own clock (`DateTime.local()`), not from the user's time zone override. The moment set by Now is still right (the time carries the browser's offset), but for a user whose override zone is far from the browser's zone, Today near midnight can pick the neighbouring day
|
||||
|
||||
## Log
|
||||
|
||||
| Date | Step | Result |
|
||||
|---|---|---|
|
||||
| | | |
|
||||
| 2026-10-22 | Survey of sections 1–3 against the code (read-only) | Findings recorded per item: n/a items ticked with reasons, evidence added, MiniProfiler found present |
|
||||
| 2026-10-22 | Integration baseline, `eb607d34` (unmodified, .NET 8.0.24, fresh test database) | 103 tests: 96 passed, 2 failed (the two known server bugs), 5 skipped |
|
||||
| 2026-10-22 | Case 4676 data-list SQL injection fix (.NET 8) | Integration suite same as baseline. API probe: valid Integer, Decimal and Roles filters return rows; `0 OR 1=1`, `0); select pg_sleep(3); --`, `1)=1 OR (1` and similar are rejected with `ArgumentException` before any SQL runs |
|
||||
| 2026-10-22 | `AdminConnectionString` via `NpgsqlConnectionStringBuilder` (.NET 8) | Boot database check passes; integration suite same as baseline. Old vs new on sample strings: old turned role `AyaNova` and password `AyaNova@1 x` into `postgres`, and host `ayanova.example.com` into `postgres.example.com`; new changes only `Database`. The section 4 role test still applies |
|
||||
| 2026-10-22 | Retarget `net10.0` + framework-matched `Microsoft.*` 10.0.13 + Npgsql EF 10.0.3 | Builds, 0 errors (warnings: CA2022 `FileUtil.cs:875`; NU1902/NU1903 MailKit/MimeKit/Pkcs). Boots on .NET 10.0.3, licence accepted. Suite, first run after boot: 95 passed, 3 failed: the two known plus `VendorCrud.CRUD`, caused by the existing `FormFieldReference` start-up race (see Deferred); `VendorCrud` passes alone. Full rerun: 96 passed, 2 failed (known), 5 skipped = baseline. No EF warning-as-error thrown. Licence, report and E2E gates to run next on this build |
|
||||
| 2026-10-22 | Licence gate (`ayanova.tests`) on `6d6d4d9a` (.NET 10) and on unmodified `eb607d34` (.NET 8 runtime, temporary worktree) | Both 730/730 passed. 726 v8 rows: 389 verify and parse, 337 rejected as not licensed for this build (225 subscription keys, 112 perpetual with maintenance before the build date), 0 signature, format or parse errors. Per-row outcomes identical between the two builds |
|
||||
| 2026-10-22 | Report and E2E baseline gates | Not run, by decision: reports to be verified visually by John; E2E deferred to a later smoke test |
|
||||
| 2026-10-22 | `raven-launcher` → `net10.0` (`d2079c9`) | Release-style publish (`-r win-x64 --self-contained`, trimmed): no warnings, 42 files / 20.3 MB (was 37 / 18.7 MB on .NET 8). Smoke run in a scratch layout: prints version 8.3.0, parses `config.json`, reaches the `pg_ctl.exe` start. Installer test still in section 4 |
|
||||
| 2026-10-22 | MailKit/MimeKit 4.5.0 → 4.18.1, BouncyCastle 2.3.1 → 2.7.0, Pkcs 10.0.13 pinned | Builds (only CA2022 left); no vulnerable packages. Licence gate 730/730, per-row outcomes identical to before. Mail end to end: `test-smtp-settings` sent through `Mailer` (connect, AUTH, send) to a local SMTP sink: well-formed `text/plain; charset=utf-8` message received. Integration suite = baseline. PDF-attachment path not exercised (report emailing) |
|
||||
| 2026-10-22 | Swashbuckle 6.5.0 → 10.3.1, OpenAPI.NET 1.6 → 2.7.5 | Builds (only CA2022). `/swagger/v8/swagger.json` compared with the pre-change document: same 315 paths, 462 operations (ids, parameters, request bodies, response codes) and 137 schemas; same global Bearer requirement and scheme. Only change: `openapi` 3.0.1 → 3.0.4. `/api-docs` UI serves. Integration suite = baseline |
|
||||
| 2026-10-22 | NLog 5.3 → 6.2.2; archive suffix `-{0}`; log viewer sorted by file date | Builds (only CA2022). Server at Trace level, logging to a scratch folder seeded with NLog 5 archives `-0` to `-8`, through suite runs: `log-ayanova.txt` current, 9 archives `log-ayanova-23` to `-31`, old files removed; `GET log-file` lists the current file, then `-31` down to `-23`. Integration suite = baseline. In one of six Trace-level runs `LogFiles.MostRecentLogShouldFetch` most likely failed: a just-rolled current file can hold only TRACE/DEBUG lines (not reproduced; not at Info level) |
|
||||
| 2026-10-22 | PuppeteerSharp 17 → 25.15.0; launch arguments as separate elements | Builds (only CA2022). Rendered all 24 work order and customer reports through the API (`render-job`, poll, download) for records 1 and 2: on PuppeteerSharp 17 (baseline), on 25 with default arguments, and on 25 with `AYANOVA_REPORT_RENDER_BROWSER_PARAMS="--no-sandbox --disable-gpu"`. 24/24 ok each time, page counts identical; PDFs 5–20% smaller with Chrome 155. Integration suite = baseline. PDFs kept in the scratchpad for John's visual check |
|
||||
| 2026-10-22 | John's visual check of the PuppeteerSharp 25 PDFs; `--lang` fix | Identical apart from report 80: date month/day flipped (default locale, see section 1). Probe of Chrome 124/155 with each argument set confirmed the cause. With `--lang=<server culture>`: John confirmed report 80 shows 22/10/2026 again; 22/22 render, page counts = PuppeteerSharp 17, launch arguments logged as `--no-sandbox --lang=en-CA`. Integration suite: 95 passed, 3 failed: the two known plus `FormCustom.FormCustomUpdate`, which the server log shows was the `FormFieldReference` start-up race again (`Vendor is not valid` at the first form-field use after boot; see Deferred), not this change. **Never render reports 81 (sends real email via the server's SMTP settings) or 86 (writes to the work order) in tests**; 81 ran three times on 2026-10-22 and caused bounces |
|
||||
| 2026-10-22 | Asp.Versioning 8.1.0 → 10.2.1, explicit `.AddMvc()` | Builds; new warnings AV0015/AV0016 left (see section 1) besides CA2022. `swagger.json` identical to the Swashbuckle 10 document (315 paths, 462 operations, 137 schemas, 0 differences). `api/v8/…`, `api/v8.0/…` and `?api-version=8.0` return 200 with `api-supported-versions: 8.0`; `api/v9/…` returns 404 (8.1 response for v9 not checked). Integration suite = baseline |
|
||||
| 2026-10-22 | MiniProfiler removed | Builds (only AV0015/AV0016/CA2022). No `StackExchange.Profiling`/`MiniProfiler`/`AY_PROFILER` left in the server. `/profiler/results-index` now gets the client app's `index.html` fallback like any unknown path; unauthenticated API call still 401. Integration suite = baseline |
|
||||
| 2026-10-22 | CA2022: `FileUtil.GetLinkerTimestampUtc` reads with `ReadAtLeast` | CA2022 gone (only AV0015/AV0016 left). Licence gate 730/730 incl. build-timestamp and `MExBB` checks; per-row outcomes identical to the first .NET 10 run (389 ok / 337 rejected for this build). Server boots and accepts the (trial) licence |
|
||||
| 2026-10-22 | Bogus 35.6.5, jose-jwt 5.3.0, QRCoder 1.8.1, Newtonsoft.Json 13.0.4 direct; `appsettings.json` levels; ASP0019 n/a | Builds (only AV0015/AV0016). All packages current, none vulnerable. Login JWT still `HS256` and accepted on every authenticated call; `GET auth/totp` returns a valid 183×183 QR PNG and leaves 2FA disabled. Integration suite = baseline. Bogus (evaluation seeder) compiled only: running the seeder replaces the test data |
|
||||
| 2026-10-22 | Backup `pg_dump` without a shell; password in `PGPASSWORD` | On-demand backups through the API into a scratch folder with a space in its name, checked with `pg_restore --list`. Old code as `postgres`: 803 entries. New code as `postgres`: 803 entries. Failing stand-in `pg_dump` (exit code 2): reported as `BACKUP ERROR`, no file, no password in the log. Temporary superuser role `"AyaNova"` (named like the database) with password `p @:/?#% x`: old code failed (`too many command-line arguments (first is "@:/?#%")`), left a 0-byte backup file and echoed part of the password into the log; new code 803 entries, no errors. Role dropped afterwards. Integration suite = baseline. Linux not tested here: the stderr gap there is closed by the same code (no shell, exit code checked) |
|
||||
| 2026-10-22 | Drop-and-recreate: CREATEDB check before the drop | Throwaway non-superuser role `ayatest_nocreatedb` owning throwaway database `ayatest_erase`, server data path in scratch (the erase also clears the attachments folder). Without CREATEDB: boot stops with the new E1000 message, database still there. Old sequence by hand as the same role: `DROP DATABASE` succeeds, `CREATE DATABASE` fails "permission denied", database gone. With CREATEDB: erased and recreated (oid 145595 → 145596), schema 20 built, boot complete. Database and role dropped afterwards. Integration suite not rerun: the change is only reached with the erase setting |
|
||||
| 2026-10-22 | Report browser downloaded under the data path | Browsers in `bin` moved aside; server with an empty scratch data folder. 22/22 reports render; Chrome and ChromeHeadlessShell 155 downloaded into `<data>/report-browser`, launched from there (logged), nothing written to `bin`. Second run: first render 1.6 s (was 25 s with the download), no new files: download reused |
|
||||
| 2026-10-22 | Specific error for report browser download / launch failures | Download blocked (a file named `report-browser` in the data folder): job failed with `rendererror.exception` "…could not be downloaded to "…\report-browser" (check internet access and that the folder is writable): … Cannot create …", Error in the log, server still up. Launch with `AYANOVA_REPORT_RENDER_BROWSER_PATH` pointing nowhere: "…could not be started from "C:\no-such-folder\chrome.exe" with arguments '--no-sandbox --lang=en-CA' …: Browser was not found…", server still up. Normal: 22/22 reports, page counts = PuppeteerSharp 17; integration suite = baseline |
|
||||
| 2026-10-22 | Data-list date filters `>=`, `<`, `!=`; null check | `WorkOrderServiceDate` through `/data-list`, 590 work orders. Minute with 6 records off second 0, before: `>=` 142 (= `>`), `<` 448 (= `<=`), `!=` 590 (all). After: `>=` 148, `<` 442, `!=` 584; identities `>=` = `>`+K, `<` = `<=`−K, `!=` = all−K hold. Same identities hold for a minute with 5 records exactly on second 0. Integration suite = baseline |
|
||||
| 2026-10-22 | `raven-test-integration` → `net10.0` (`2c0cfc4`), no package changes | Builds with no warnings; suite runs as `net10.0` against the .NET 10 server: 96 passed, 2 known failures, 5 skipped = baseline. FluentAssertions kept at 5.10.3 (8+ is commercially licensed) |
|
||||
| 2026-10-23 | Ubuntu 24.04.5 test droplet (`net10port.onayanova.com`, 1 GB): 8.2.4 on .NET 8 set up by John like a customer (subscription trial licence, sample data, PostgreSQL 18.6 from PGDG) | `aspnetcore-runtime-10.0` 10.0.13 from `noble-updates/main`. In-place upgrade to 8.3.0: boots on .NET 10.0.13, schema 20, licence kept. Reports first **failed to launch** (full Chrome 155, crashpad needs a writable home; see section 1); fixed by launching `chrome-headless-shell` (measured 3–4× faster and smaller): 22/22 on Windows (page counts = PuppeteerSharp 17, 1.14 s avg vs 1.8 s) and on the droplet; `ldd` no missing libraries; cancelled render leaves no browser processes. Integration suite through an SSH tunnel: 28 failures, all from **SuperUser being deleted** by `UserWithActivityShouldNotBeDeleteable` (pre-existing bug, see Deferred); after restoring the pre-upgrade dump and excluding that test: 95 passed, 2 known failures, 5 skipped = baseline. On-demand backup 818 entries. Non-superuser role `"AyaNova"` named like the database with password `p @:/?#% x`, database restored with `--role`: boot, backup, 22/22 reports, suite = baseline |
|
||||
| 2026-10-23 | John's PDF check of the headless-shell renders; Linux date format compared with 8.2.4 | PDFs identical apart from tiny font-size variations. Browser-default-locale dates (bare `toLocaleDateString()`, Chrome's `<span class="date">` in PDF headers) are `en-US` on the droplet (`10/23/2026`, `10/23/26, 7:34 AM`) but day-first on Windows (`en-CA`). Cause: Linux services run with `LANG=C.UTF-8` (DigitalOcean default), so .NET sees an invariant culture and no `--lang` is added. Same reports rendered on John's 8.2.4 devtest server (Ubuntu 24.04, same `LANG`): `10/23/2026` and `10/23/26, 8:13 AM`, so **unchanged from 8.2.4**. A hosted customer wanting another default can set `LANG` in the service unit; not a port item |
|
||||
| 2026-10-23 | Fresh Ubuntu 26.04.1 droplet (rebuilt `net10port`, updated by John), updated setup script with the 8.3.0 build | Archive: `aspnetcore-runtime-10.0` 10.0.13 in `resolute`/`resolute-updates` main (no .NET 8 at all, so 8.2.4 cannot run on 26.04); PostgreSQL 18 in Ubuntu's archive but the PGDG repository is kept for one source everywhere; `apt-key` absent (apt 3.2); one Chrome library renamed. Script exit 0: time zone, swap, unattended upgrades, no-phasing, .NET 10.0.13, PostgreSQL 18.6 (pgdg26.04), unique secrets in place and accepted by PostgreSQL, `config.json` 640, AyaNova 8.3.0 boots unlicensed on a new database (schema 20), nginx + Let's Encrypt certificate, HTTPS 200. 11 updates deferred by the no-phasing setting, as designed. Section 4 tests on 26.04 pending John's licence and sample data |
|
||||
| 2026-10-23 | Section 4 on the 26.04 droplet (licensed and sample data by John) | 22/22 reports (0.9–2.7 s); only `ChromeHeadlessShell` downloaded; `ldd` no missing libraries; cancelled render: browser processes gone within about 25 s (cleanup slower than on 24.04, nothing orphaned). On-demand backup 818 entries. Suite excluding the SuperUser-delete test (SuperUser again had no related records): 95 passed, 2 known failures, 5 skipped = baseline. Restricted role `"AyaNova"` (no superuser, no CREATEDB, password `p @:/?#% x`), database restored with `--role`: boot, backup 818 entries, 22/22 reports, suite = baseline; `config.json` stays 640 `root:www-data` |
|
||||
| 2026-10-23 | `lan.iss` Hosting Bundle prompt → .NET 10 | Message says 10.x, link opens `dotnet/10.0`. `standalone.iss` has no runtime prompt (launcher and server are self-contained). Not yet built into an installer |
|
||||
| 2026-10-23 | `lan.iss` and `standalone.iss`: no more `{app}\Chrome` / `{app}\ChromeHeadlessShell` folders | The report browser downloads to `<data>\report-browser` (`%ProgramData%\ayanova`, users-modify in both installers), so the program-folder browser folders were created empty and unused. Uninstall now deletes those folders themselves (was only their contents), clearing what 8.2.x left behind and letting the program folder be removed. Check in the section 4 installer test: fresh install has no `Chrome` folders; upgrade from 8.2.4 then uninstall removes them |
|
||||
| 2026-10-23 | Build tooling: `build-release.bat` logs all output to `build-output.txt` (UTF-16, overwritten each build, git-ignored) as well as the console; sets `PYTHONUTF8=1` (mkdocs-material 9.7 banner otherwise crashes MkDocs when output is piped). `raven-client` `vue.config.js`: Sass `quietDeps` | Wrapper tested on a dummy script: live console output, stderr captured, Unicode intact. Client build: Sass deprecation warnings 475 → 33 (the rest are Vuetify component styles compiled as the entry stylesheet, which `quietDeps` cannot tell apart); CSS output byte-identical to the 8.3.0 release build |
|
||||
| 2026-10-23 | Linux packages from `build-release.bat` on a fresh Ubuntu 24.04.5 droplet (`devtestn10`): 8.2.4 subscription server set up by John with the deployed setup script, trial licence, sample data, one report rendered on 8.2.4 | **Subscription zip**, the manual's steps 1.5 and 2 with the built zip: .NET 10.0.13 from `noble-updates`, boots 8.3.0, schema 20, trial licence kept, 0 errors; 22/22 reports (1.5–2.8 s, first 7.5 s with the download); browser in `data/report-browser` (`ChromeHeadlessShell` only, 272 MB); old `Chrome`/`ChromeHeadlessShell` removed with the manual's command. The E1090 in the log is the 8.2.4 server, restarted by `needrestart` during `apt-get upgrade` and cancelled mid-boot by the manual's `systemctl stop`; not 8.3.0. DataProtection ephemeral-key warnings at boot: same in 8.2.4. **Perpetual server zip**, fresh folder, own database, port 7576, as `www-data`: boots unlicensed (E1020), creates the database, 0 errors, login page 200. **Desktop zip**, the desktop manual's steps as an ordinary user (`sudo ./AyaNova`): `AyaNova` apphost runs on .NET 10, boots, creates the database, 0 errors, login page 200; data goes to `/root/.ayanova-data` (see Deferred). Test databases, folders and user removed afterwards |
|
||||
| 2026-10-23 | `standalone.iss`: create the 13 empty folders PostgreSQL 14 needs in a new database (`pg_notify`, `pg_tblspc`, `pg_twophase`, `pg_stat`, `pg_stat_tmp`, `pg_commit_ts`, `pg_dynshmem`, `pg_replslot`, `pg_serial`, `pg_snapshots`, `pg_logical\mappings`, `pg_logical\snapshots`, `pg_wal\archive_status`) | **Release blocker found in the Sandbox test:** fresh standalone installs built on this PC failed, `pg_ctl` "could not start server", `postgres.log` "could not open directory pg_notify". `dist\win-x64\postgres-empty-database` was checked out from Git on 2026-02-19 and Git does not keep empty folders; compared with a reference `initdb` of the bundled PostgreSQL 14, exactly these 13 were missing. 8.2.4 (built on the old machine) is not affected; upgrades are not affected (existing database kept). Fixed in the installer rather than on disk so a fresh checkout cannot break it again. Fixed installer: fresh install in a clean Sandbox starts PostgreSQL first time, schema 20 created, boot complete |
|
||||
| 2026-10-23 | Windows standalone installer in Windows Sandbox (clean Windows 11, `C:\temp\ayanova-sandbox\ayanova-test.wsb`, checks by `check.ps1`): 8.2.4 production installer, trial licence with sample data (John), upgrade with the 8.3.0 build, uninstall, fresh install | **8.2.4 install:** needed the Visual C++ runtime first (see Deferred; `vc_redist.x64.exe` crashed the Sandbox, the three DLLs copied into `local-postgres\bin` worked); report 10 Customer Work Order 1.6 s, `Chrome`/`ChromeHeadlessShell` in the program folder. **Shutdown:** launcher crash at `Program.cs:98`, 7 `postgres` left running (see Deferred). **Upgrade to 8.3.0:** Setup "files in use" (PostgreSQL Server ×7), automatic close failed, installer cancelled itself; after the leftover processes were gone, upgrade went through: 8.3.0 (`a223a2bc`), `net10.0`, schema 20, trial licence and data kept, PostgreSQL recovered from the unclean stop; report 10 rendered in 13.9 s with the browser download, browser in `%ProgramData%\ayanova\report-browser`; runtime `10.0.3` (see section 5). **Uninstall** (delete data: yes): `Chrome`/`ChromeHeadlessShell` removed with the program folder, data folder removed; only the hand-copied runtime DLLs left behind. **Fresh install:** failed to start PostgreSQL (missing empty folders, fixed in `standalone.iss`, see the previous row); with the fixed installer: starts first time, schema 20 created, no `Chrome` folders |
|
||||
| 2026-10-23 | PostgreSQL link and version for Windows network installs | The PostgreSQL install guide linked from the LAN installer prompt and the LAN/IIS manual pages (`postgresqltutorial.com/install-postgresql/`) now fails (Cloudflare error; the site moved to a database vendor, Neon). Replaced with the official `https://www.postgresql.org/download/windows/` in `lan.iss` and both pages; prompt text "Open the PostgreSQL download page now?". `AYANOVA_BACKUP_PG_DUMP_PATH` in `lan-install-config.json` and the manual examples: PostgreSQL 16 → 18, with a note to change the number to the installed version. Changelog lines added. Docs build clean; LAN installer compiled to `C:\temp\ayanova-sandbox\ayanova-windows-x64-lan-setup-pglink.exe` for the LAN test |
|
||||
| 2026-10-23 | Windows LAN installer in a fresh Windows Sandbox, following `ops-install-windows-lan.md`; installer from this branch with the PostgreSQL link fix (`ayanova-windows-x64-lan-setup-pglink.exe`) | Hosting Bundle prompt opened the .NET 10 page; .NET 10.0.13 Hosting Bundle installed (very slow in the Sandbox, normal there). PostgreSQL prompt opened `postgresql.org/download/windows/`, which links to EDB; PostgreSQL 18.6-5 installed (it installs the Visual C++ runtime itself, no Sandbox crash with vGPU off). `config.json` edited (password, own JWT secret; the Sandbox has no Notepad, edited with PowerShell). AyaNova 8.3.0 boots on .NET 10.0.13, database created at schema 20, no JWT warning, no `Chrome` folders. After licensing (John), an on-demand backup succeeded with the template's `PostgreSQL\18\bin` pg_dump path. Windows Firewall prompt on every start in the Sandbox (fresh Windows each session), expected |
|
||||
| 2026-10-23 | Checklist tidy-up: older unticked items in sections 0–5 checked against current evidence | `main` = `origin/main` = `net8-baseline` in `raven` and `raven-client`; tag on both remotes. Licence gate 730/730 at `181b1e44`. No EF Core warning raised as an error in any log (local dev server, Sandbox, `devtestn10`). `System.Text.Json` 5.0.2 gone from the graph. JwtBearer 10.0.13 done. Report and E2E baselines marked waived (John); Cypress stays open in section 4 and now also covers the UI side of the EF Core screen-by-screen item. Restore manual compared: fine for the default `postgres` setup; dedicated-role note added to Deferred. Stale "still to do" remarks removed. Still open: relative date tokens (section 3, decision needed), Cypress, and the section 5 release steps |
|
||||
| 2026-10-23 | `net10-port` merged into `main` (`--no-ff`, no tag) in `raven`, `raven-client`, `raven-launcher`, `raven-test-integration` (John) | Pre-checks: all clean and pushed; `main` = `origin/main` with no commits outside `net10-port`. Post-checks: `git diff net10-port main` empty in all four; pushed. `CLAUDE.md` "Current work" now describes the 8.3.0 release on `main`; stack table updated to .NET 10; dependency rule no longer refers to the port branch. `net10-port` branches kept until after release (section 5) |
|
||||
| 2026-10-26 | Final gate, release candidate build, Cypress | See section 5. Performance, same suite and browser, devtest (8.2.4) and `devtestn10` (8.3.0), same droplet size and region (San Francisco): generating the test data 1:43 on 8.2.4, 0:42 and 0:45 on 8.3.0 (about 2.4 times faster; mostly server work); the 21 UI specs that passed in both, 224 s and 200 s (about 10% faster; much of a UI test is Cypress itself). `build-release.bat` failed twice at the subscription zip while an Explorer or PowerShell window was open on `dist\installers` (folder in use); clean once closed |
|
||||
| 2026-10-26 | Final build, staging, devtest (RELEASE-PROCEDURE.md steps 3 to 5) | Final build from `05cf4df6` (changelog and manual date only since the release candidate): all five packages 8.3.0, runtime 10.0.14. Staged in `download/next` and `docs-next` on ayanova.com (new droplet `ayanova-2`; its host key changed with the cutover, fingerprint confirmed): SHA-256 of all five files match the build, 665 docs files. Found: the live `latest-version.json` still announces 8.2.3, so installations were never told about 8.2.4; the 8.3.0 announcement covers it. devtest (snapshot first): OS update, .NET 10.0.13, `harden-830.sh` taken from the procedure, the procedure's next-version upgrade command as written, reboot: 8.3.0 boots, schema 20, trial kept, 127.0.0.1 only, program folder read only, `config.json` 640 (was 644), nothing blocked. Cypress on devtest 27/27. Tagged `8.3.0` in `raven` (`05cf4df6`), `raven-client`, `raven-launcher`; publishing, droplet upgrades and the announcement follow on Thursday 2026-10-29 (John) |
|
||||
|
||||
@@ -48,7 +48,7 @@ Ground Zero's own test droplet first, then customers through the day by time zon
|
||||
|
||||
- **Workstation:** Beelink SER9 Pro+, 32 GB, Windows 11 Pro. Backup: ThinkPad X13 Gen 3
|
||||
- **Editors:** VS Code for everything except QBI, which needs Visual Studio for the Windows Forms designer
|
||||
- **Build VM:** a complete Windows 11 build environment in VirtualBox, exported and backed up to several drives (`devdocs/build/DEV-VM-SETUP.md`)
|
||||
- **Builds:** releases are built on the workstation; the backup laptop is the fallback. A VirtualBox build VM (`devdocs/build/DEV-VM-SETUP.md`) was set up in early 2026 but is deprecated: going fully virtual could not cover QBI and some other parts
|
||||
- **Source control:** Git on a self-hosted Gitea on DigitalOcean. Moved from SVN in early 2026
|
||||
- **Location:** off-grid, Starlink internet, solar power. The workstation is not left running overnight in winter, so do not plan work that depends on overnight runs on it
|
||||
|
||||
|
||||
@@ -7,13 +7,13 @@ the original plan (February–March 2026). Statuses are as of October 2026; any
|
||||
| Step | What | Status |
|
||||
|---|---|---|
|
||||
| — | SVN → Git, self-hosted Gitea | ✅ Done |
|
||||
| — | Build process documented; Docker builds for front end and docs; Windows build VM exported and backed up | ✅ Done |
|
||||
| A1 | E2E tests with Cypress 9.7.0, targeting remote devtest servers, with version validation and environment-based config | **[unverified]**: the March notes listed it as both done and in progress |
|
||||
| — | Build process documented; Docker builds for front end and docs; Windows build VM exported and backed up (VM later deprecated: builds run on the workstation, see `devdocs/build/BUILD.md`) | ✅ Done |
|
||||
| A1 | E2E tests with Cypress 9.7.0, targeting remote devtest servers, with version validation and environment-based config | **[unverified]**: the suite exists in `raven-test\e2e` (last commit 2026-02-22), but the March notes listed it as both done and in progress |
|
||||
| A2 | Fix the outdated integration tests in `raven-test-integration` | ✅ Done, March 2026: compiles on `net8.0`, broken tests removed or rewritten |
|
||||
| A3 | Data-list filter and sort integration tests | ✅ Done, March 2026, together with Tier 1–3 gap coverage. February notes still called them stubs; the March notes supersede that **[unverified]** |
|
||||
| A3 | Data-list filter and sort integration tests | ✅ Done, March 2026, together with Tier 1–3 gap coverage. `raven-test-integration\DataList\DataListOperations.cs` holds 15 real tests, not stubs |
|
||||
| — | Fix the three server bugs the integration suite is waiting on: case 4653, case 4654, the contract issue | Open |
|
||||
| ▶ | **.NET 10 port of the back end** | **Current**: `NET10-PORT.md` |
|
||||
| B | Git baseline: tag plus refactor branch | **[unverified]**. The port's `net8-baseline` tag covers the tag half |
|
||||
| B | Git baseline: tag plus refactor branch | Tag half done: `net8-baseline` exists in `raven` and `raven-client`. Refactor branch **[unverified]** |
|
||||
| C | Architecture review | Not started |
|
||||
| D | Refactor: remove redundant code, simplify, improve performance. No behaviour changes | Not started |
|
||||
| E | Lower-level tests: unit and integration | Not started |
|
||||
@@ -37,6 +37,13 @@ In this order, each step tested before the next:
|
||||
5. Webpack → Vite
|
||||
6. Optional: Options API → Composition API; TypeScript for new files only
|
||||
|
||||
## Step F, customer manual build (not part of the current port)
|
||||
|
||||
MkDocs 1.x is unmaintained, and Material for MkDocs does not work with MkDocs 2.0 (the
|
||||
build prints a warning to that effect since mkdocs-material 9.7). MkDocs is pinned below 2
|
||||
(`devdocs/build/BUILD.md`). Later: move the manual and customer docs to Zensical, the
|
||||
Material team's successor, and check every page and link after the move.
|
||||
|
||||
## Before step D starts
|
||||
|
||||
1. The integration suite passes, apart from the three known server bugs
|
||||
|
||||
957
devdocs/modernization/SECURITY-REVIEW.md
Normal file
957
devdocs/modernization/SECURITY-REVIEW.md
Normal file
@@ -0,0 +1,957 @@
|
||||
# AyaNova v8 security review
|
||||
|
||||
Whole-codebase security review requested by John (2026-10-23). Done in passes, one area per
|
||||
session; each pass ends with its findings written here and committed. **Review only: no code
|
||||
changes.** Each fix John approves becomes its own case.
|
||||
|
||||
Findings in this file describe weaknesses in shipped software. The repository is private;
|
||||
keep it that way while findings are open.
|
||||
|
||||
## Status
|
||||
|
||||
| Pass | Area | Status |
|
||||
|---|---|---|
|
||||
| 1 | Threat model and attack-surface map | Done 2026-10-23 |
|
||||
| 2 | Unauthenticated routes, login, tokens, password reset, 2FA | Done 2026-10-23 |
|
||||
| 3 | Customer contact users: what they can reach and read | Done 2026-10-23 |
|
||||
| 4 | Report rendering (templates, the server-side browser, report data and download routes) | Done 2026-10-23 |
|
||||
| 5 | File handling: attachments, uploads, exports, logs, backups, restore, translations, logo | Done 2026-10-23 |
|
||||
| 6 | Raw SQL: data lists, saved filters, search, KPI, schedule, pick lists | Done 2026-10-23 |
|
||||
| 7 | Staff roles: the authenticated routes without a role check in the action (list below) | Done 2026-10-23 |
|
||||
| 8 | Email and notifications (`direct-smtp`, templates, delivery) | Done 2026-10-23 |
|
||||
| 9 | Client (XSS, token storage, `npm audit` triage), CORS, headers, Swagger UI | Done 2026-10-23 |
|
||||
| 10 | Deployment: installers, Linux setup script, nginx, systemd, config and data permissions | Done 2026-10-23 |
|
||||
| 11 | Sockeye public routes (licence fetch, trial requests) | Done 2026-10-23 |
|
||||
|
||||
The order is by risk to internet-facing servers. Findings from any pass go in **Findings**.
|
||||
|
||||
## Scope
|
||||
|
||||
- **In:** the AyaNova server (`raven/server/AyaNova`) and client (`raven-client`); the Windows
|
||||
installers and launcher; Linux packages and the hosted subscription server setup script
|
||||
(`sockeye-client/src/views/biz-subscription-server.vue`); the configuration templates.
|
||||
- **Sockeye:** only its public routes (licence fetch and trial requests). Its admin UI is used
|
||||
by John alone (revisit if agents use it later).
|
||||
- **Out:** QBI.
|
||||
|
||||
## Threat model
|
||||
|
||||
**Deployments**
|
||||
|
||||
| Deployment | Reachable by | Notes |
|
||||
|---|---|---|
|
||||
| Hosted subscription droplet (Ubuntu, nginx + Let's Encrypt, `www-data`) | The internet | Most exposed. One company per droplet |
|
||||
| Self-hosted Windows IIS | Often the internet | The manual recommends IIS when there is internet access |
|
||||
| Self-hosted Linux server | LAN or internet | Customer's own reverse proxy, if any |
|
||||
| Windows LAN / Linux desktop | Local network | Runs as the logged-in user (Linux desktop: as root with `sudo ./AyaNova`) |
|
||||
| Windows standalone | The PC itself | Mostly evaluation (John) |
|
||||
|
||||
**Who might attack**
|
||||
|
||||
1. **Anyone on the internet** (no login): 14 anonymous routes, the static client, Swagger UI,
|
||||
health check.
|
||||
2. **A customer contact** (customer-user login, roles `Customer`/`CustomerRestricted`):
|
||||
should see only its own customer's data. The highest-value question in this review: can
|
||||
one customer's login read or change another customer's data, or staff data?
|
||||
3. **A staff user with limited roles**: should not exceed them (e.g. a restricted tech
|
||||
reaching accounting, ops or user admin).
|
||||
4. **Someone holding a leaked token**: a JWT or a download token (`?t=` in URLs, so it can
|
||||
land in browser history and proxy logs).
|
||||
5. **A local user** on a Windows or Linux machine running AyaNova.
|
||||
6. **A report template author**: report templates are JavaScript run in a browser on the
|
||||
server, with access to report data and the API.
|
||||
|
||||
**What is at stake**: customer business data (customers, work orders, invoices,
|
||||
attachments), user logins and passwords, the company's email reputation (SMTP), the licence,
|
||||
the server itself (code execution, the database, backups), and availability.
|
||||
|
||||
## Attack-surface map
|
||||
|
||||
### Authentication and authorisation design
|
||||
|
||||
- **Login**: `POST auth` returns a JWT (HS256, `jose-jwt`), signed with `AYANOVA_JWT_SECRET`
|
||||
(random per boot if not set). Validation (`Startup.cs:255-279`): signature, issuer
|
||||
`ayanova.com`, expiry, zero clock skew; audience not checked.
|
||||
- **One session per user**: the user's latest token is stored (`CurrentAuthToken`); the
|
||||
middleware (`Startup.cs:366-451`) rejects any other valid token with E2004, except on
|
||||
`/auth` and `notify/hello`, and except local report-render requests: host `127.0.0.1`,
|
||||
`Request.IsLocal()` and an `rpl` claim. Behind the hosted nginx the client address is
|
||||
forwarded (`X-Forwarded-For`, `UseForwardedHeaders`), so remote requests are not local.
|
||||
The exception skips only the latest-token check, not signature or expiry.
|
||||
- **Roles** are read from the database on every request (middleware), not from the token.
|
||||
**There is no route-level role attribute and no global "login required" policy**: each
|
||||
controller has `[Authorize]` (login only) and each action calls `Authorized.Has…Role(...)`
|
||||
itself. A missing call means any logged-in user, customer contacts included.
|
||||
- **Download routes** (`?t=`) take a per-user random download key set at login (`DlKey`,
|
||||
with expiry, `AuthController.cs:272-276`) instead of a JWT. Some check only that the key
|
||||
belongs to some active user (export, report, translation); others resolve the user and
|
||||
check rights (attachments, backups, logs). To verify in passes 4 and 5.
|
||||
- **CORS**: any origin, any method, any header (`Startup.cs:167-174`). Tokens travel in the
|
||||
`Authorization` header, not cookies, which limits the impact; checked in pass 9.
|
||||
- **Swagger UI** is public at `/api-docs` with the full API description (pass 9).
|
||||
|
||||
### Routes
|
||||
|
||||
Generated with `devdocs/modernization/security/routemap.py` (`python -I routemap.py
|
||||
<Controllers folder> <out.tsv>`): **460 actions** in 71 controllers. Regenerate it in later
|
||||
passes rather than trusting these counts if the code has changed.
|
||||
|
||||
| Access | Actions |
|
||||
|---|---|
|
||||
| Anonymous (`[AllowAnonymous]`) | 14 |
|
||||
| Download key only (no attribute; check `?t=`) | 2 (`attachment/download/{id}`, `backup/download/{fileName}`) |
|
||||
| Login required (`[Authorize]`) | 444 |
|
||||
|
||||
Controllers without a class-level `[Authorize]` (`Attachment`, `Backup`, `ServiceBank`) were
|
||||
checked: every action has its own `[Authorize]` or is a download-key route; `ServiceBank` is
|
||||
entirely commented out.
|
||||
|
||||
**Anonymous routes** (pass 2 unless noted)
|
||||
|
||||
| Route | Action |
|
||||
|---|---|
|
||||
| `GET /` (API root) | Index |
|
||||
| `POST auth` | PostCreds (login) |
|
||||
| `POST auth/tfa-authenticate` | TfaAuthenticate |
|
||||
| `POST auth/reset-password` | ResetPassword |
|
||||
| `GET export/download/{fileName}` | DownloadAsync (download key; pass 5) |
|
||||
| `GET health` | Get |
|
||||
| `POST license/lc` | lc |
|
||||
| `GET log-file/download/{logname}` | DownloadLog (download key; pass 5) |
|
||||
| `GET logo/{size}` | DownloadLogo |
|
||||
| `GET notify/hello` | GetPreLoginPing |
|
||||
| `GET report/download/{fileName}` | DownloadAsync (download key; pass 4) |
|
||||
| `GET report/export/{id}` | DownloadTemplate (download key; pass 4) |
|
||||
| `POST translation/subset/{id}` | SubSet |
|
||||
| `GET translation/download/{id}` | DownloadTranslation (download key; pass 5) |
|
||||
|
||||
**Logged-in routes with no role or user-type check in the action itself** (53, heuristic:
|
||||
the check may sit in the business layer, or the route may be harmless, such as lists,
|
||||
own settings or own notifications). Each is resolved in pass 7 unless an earlier pass covers
|
||||
it: `GET server-info`, `GET role-rights`, `GET build-mode`, `POST auth/change-password`,
|
||||
`GET auth/totp`, `POST auth/totp-validate`, `GET authorization-roles/list`,
|
||||
`GET/POST/DELETE data-list-column-view…`, `GET data-list/listkeys`, `GET data-list/listfields`,
|
||||
`GET/PUT/POST/DELETE data-list-filter…`, `GET enum-list/list/{enumkey}`,
|
||||
`GET enum-list/listkeys`, `GET form-field-reference/{key}`, `GET global-biz-setting/client`,
|
||||
`GET job-operations/status|progress|logs/{gid}`, `POST job-operations/request-cancel`,
|
||||
`POST kpi`, `POST license/permanently-erase-all-data[-keep-tax-codes]`,
|
||||
`GET notify/new-count`, `GET notify/app-notifications`, `DELETE notify/{id}`,
|
||||
`POST notify/direct-message`, `POST notify/direct-smtp`, `GET notify-subscription/list`,
|
||||
`GET pick-list/template/list`, `GET pick-list/template/listfields/{ayaType}`,
|
||||
`POST report/data`, `POST report/upload`, `POST schedule/svc`, `POST schedule/user`,
|
||||
`POST search`, `GET server-state`, `GET tag-list/list`, `GET tag-list/cloudlist`,
|
||||
`GET translation/{id}`, `GET translation/list`, `GET translation/translationkeycoverage`,
|
||||
`POST translation/subset`, `POST translation/upload`, `POST trial/seed`, `GET user/amsu`,
|
||||
`GET workorder/customer/{id}`.
|
||||
|
||||
Already resolved from that list in pass 1:
|
||||
- `license/permanently-erase-all-data[-keep-tax-codes]`: SuperUser only (user id 1) plus the
|
||||
typed confirmation `i understand` (`LicenseController.cs:248-275`). OK.
|
||||
- `trial/seed`: only with a trial licence and an empty database (only SuperUser exists then)
|
||||
(`TrialController.cs`). OK.
|
||||
- `notify/direct-smtp` and `notify/direct-message`: see F3 and F4.
|
||||
|
||||
**Highest-interest leads for later passes**: `POST search` and `POST kpi` (do they filter by
|
||||
role and customer?), `POST report/data` and the report routes (pass 4), `GET
|
||||
workorder/customer/{id}` (customer users reaching other customers' work orders?), `POST
|
||||
schedule/svc|user`, `GET user/amsu`, `POST report/upload` and `POST translation/upload` (who
|
||||
may upload; what the content can do), `job-operations/*` (another user's job logs?).
|
||||
|
||||
### Other ways in
|
||||
|
||||
- **Report templates**: user-authored JavaScript and Handlebars run in a headless Chrome on
|
||||
the server (`biz/ReportBiz.cs`), with report data, a server-issued token for API calls from
|
||||
the template (`rpl` claim), and helpers such as `ayPutToAPI` (pass 4).
|
||||
- **Raw SQL**: data-list filters and sorts (`DataList/*`; case 4676 fixed the numeric and role
|
||||
values), saved filters, search, KPI, schedules (pass 6).
|
||||
- **Files**: attachments (upload, download, move, maintenance), exports, report files and
|
||||
templates, translation upload and download, logo, log files, backups and their deletion
|
||||
(passes 4 and 5).
|
||||
- **Background jobs**: backup (`pg_dump`, fixed in this release to run without a shell),
|
||||
notifications, licence fetch (outbound to the licence servers), report rendering, seeding.
|
||||
- **Email**: SMTP settings, notification templates, `direct-smtp` (pass 8).
|
||||
- **Configuration and secrets**: `config.json` (database password, JWT secret), logs (do
|
||||
they contain tokens or passwords?), the boot log's config line (password redacted).
|
||||
- **Deployment**: Windows installers (folder permissions), the launcher, the hosted setup
|
||||
script, nginx headers, the systemd unit (no hardening options), Linux desktop running as
|
||||
root (pass 10).
|
||||
|
||||
## Findings
|
||||
|
||||
Severity: **Critical** (remote, no login, or full takeover) · **High** (any login, or one
|
||||
customer reaching another's data; or serious abuse of the company's resources) · **Medium**
|
||||
(needs a specific role, local access or an unusual setup) · **Low** (hardening, information).
|
||||
Status: *candidate* (seen in code, not yet confirmed), *confirmed*, *fixed in case NNNN*,
|
||||
*accepted*.
|
||||
|
||||
**Finding IDs are permanent:** never renumber or reuse them; John's cases refer to them. New findings
|
||||
take the next free number; a fixed or accepted finding keeps its ID and changes status.
|
||||
|
||||
| ID | Severity | Status | Title |
|
||||
|---|---|---|---|
|
||||
| F43 | **Critical** | **fixed in case 4716** | Unit pick-list variant (`customerid:<value>`) is put into SQL unparsed; any logged-in user can reach it |
|
||||
| F41 | **Critical** | **fixed in case 4710** (Sockeye; the business server gets it with 8.3.0) | F5 also in Sockeye (internet-facing admin login) and in the 8.2.x business AyaNova server |
|
||||
| F42 | High | confirmed (code) | The licence-signing RSA private keys are hardcoded in Sockeye's source and binaries |
|
||||
| F5 | **Critical** | **fixed in case 4710** | 2FA login step accepts an empty or missing temp token: the password is skipped for users with 2FA on |
|
||||
| F1 | Critical (installs that kept it) | **fixed in case 4711** | Placeholder JWT secret in the LAN/IIS and Linux server config templates |
|
||||
| F19 | Critical | **fixed in case 4718** | Any logged-in user, customer contacts included, can import report templates, whose JavaScript runs in the server's browser |
|
||||
| F21 | Critical | **fixed in case 4715** | Download file names from the URL are not checked: `report/download` and `export/download` may serve files outside the temp folder on Windows |
|
||||
| F18 | Critical | **fixed in case 4716** | Pick-list search text is put into SQL without escaping; any logged-in user can reach it |
|
||||
| F26 | Critical | **fixed in case 4717** | KPI `interval` criterion is put into SQL unchecked; reachable by staff users down to TechRestricted |
|
||||
| F13 | High | **fixed in case 4712** | Customer contacts can open any customer's work order (customer view, customer report, attachments) by id |
|
||||
| F14 | High | **fixed in case 4712** | Customer contacts can read, and by code change, delete and create, any customer's service requests |
|
||||
| F15 | High | **fixed in case 4712** | Customer contacts can list every customer (and vendors, users, parts...) with any pick-list columns, phones and emails included |
|
||||
| F16 | High | **fixed in case 4712** | `schedule/svc` and `schedule/user` have no role check: anyone logged in sees the whole service schedule |
|
||||
| F17 | Medium | **fixed in case 4712** | Notification subscriptions accept any `UserId`, and object events reach subscribers without a rights check: record names to any email address |
|
||||
| F20 | High | **fixed in case 4719** | Report templates receive the renderer's token (SuperUser for customer notification reports) and the report browser can reach any network address |
|
||||
| F27 | High | **fixed in case 4720** | Restricted roles (TechRestricted, SubContractor, SubContractorRestricted) get whole work-order items through report data, rendering and export |
|
||||
| F28 | High | **fixed in case 4713** | Users with change rights on Customer (Service, Sales, Accounting) can give customer contact logins any role, BizAdmin and OpsAdmin included |
|
||||
| F33 | High | **fixed in case 4721** | Client renders some record text as HTML without escaping (search results, work-order address dialog, unit warranty info, dialogs) |
|
||||
| F3 | High | **fixed in cases 4712 and 4723** (staff sending to any address kept, by design) | Any logged-in user, customer contacts included, can send arbitrary email through the company's SMTP server |
|
||||
| F23 | Medium | **fixed in case 4727** | Logo upload accepts any content type and the anonymous logo route serves it inline from the app's origin (stored XSS) |
|
||||
| F24 | Medium | **fixed in case 4718** | Translation import has no role check: any login can add translations |
|
||||
| F30 | Medium | **fixed in case 4724** | SMTP: the mail server's TLS certificate is never checked |
|
||||
| F31 | Medium | **fixed in case 4724** | The SMTP password is returned to read-only roles (OpsAdminRestricted, BizAdmin, BizAdminRestricted) |
|
||||
| F32 | Medium | confirmed (code) | A real-looking SMTP account and password are in the DEBUG defaults in `raven` and `sockeye` source |
|
||||
| F34 | Medium | **fixed in case 4722** | Shipped client libraries behind on security fixes: DOMPurify 2.3.8 (wiki sanitiser), Vuetify 2.6.6 |
|
||||
| F6 | Medium | **fixed in cases 4710 and 4726** | No lockout or rate limit on login or the 2FA step; no password rules |
|
||||
| F9 | Medium | **fixed in case 4725** | Default SuperUser password (by design for trials), and an anonymous route that says whether it is still set |
|
||||
| F36 | Low | **accepted** for standalone and LAN; hosted **fixed in case 4727** (`sockeye-client` setup script; existing droplets at the 8.3.0 rollout) | AyaNova listens on all interfaces on plain HTTP port 7575 in every template; hosted droplets are protected by a cloud firewall |
|
||||
| F37 | Low | accepted (John) | Standalone PostgreSQL accepts any local connection without a password (`trust`) |
|
||||
| F2 | Medium | **fixed in case 4579** (write access); read access to `config.json` and write access to the data folder accepted and documented | Windows installers give all local users modify rights on the program and data folders, `config.json` included |
|
||||
| F7 | Low | **fixed in case 4725** | Password change or reset does not end existing sessions, and makes the download key non-expiring |
|
||||
| F8 | Low | **fixed in case 4725** | Latest-token check skipped for any URL ending in `/auth` or `notify/hello` |
|
||||
| F4 | Low | **fixed in cases 4712 and 4723** | Any logged-in user can send an in-app notification to any user |
|
||||
| F10 | Low | **later case 4736** (not 8.3.0; accepted as is for 8.3.0, legal check to do) | Anyone can record acceptance of the licence agreement (`license/lc`) |
|
||||
| F11 | Low | **fixed in case 4727** | API root page shows licensee, version, licence terms (and DB ID when unlicensed) to anyone |
|
||||
| F12 | Low | **later case** (not 8.3.0) | Password hashing: PBKDF2-SHA512 with 10,000 iterations |
|
||||
| F25 | Low | **fixed in case 4727** (upload before rights check accepted) | Attachment and export gaps: upload stored before the rights check, "attached by" can be set to anyone, exports not tied to their user |
|
||||
| F29 | Low | **fixed in case 4727** | `server-info` gives every login, customer contacts included, the server DB ID and full licence details |
|
||||
| F35 | Low | **fixed in case 4727** (CSP a later case; CORS, Swagger accepted) | No security headers from the server itself (self-hosted), no CSP anywhere, CORS any origin, public Swagger UI, download keys in URLs |
|
||||
| F38 | Low | **fixed in case 4727** for hosted (`sockeye-client` setup script; existing droplets at the 8.3.0 rollout); Docker files removed; desktop running as root in Deferred | Linux hardening: no systemd sandboxing, service user can replace program files, desktop runs as root, unused Docker files with a fixed database password |
|
||||
| F39 | Low | AyaNova half **fixed in case 4727**; Sockeye half a later case | Licence fetch is one-shot by DB ID, and the DB ID is shown anonymously while unlicensed: someone else can take a new key first |
|
||||
| F40 | Low | **fixed in case 4681** (Sockeye) | Trial request routes: no rate limit, non-cryptographic verification codes, a fixed bot-check value |
|
||||
| F22 | Low | **fixed in case 4727** (job owner check and Chrome sandbox accepted) | Minor report and job route gaps: template export by any download key, job status, logs and cancel by GUID without an owner check, browser without sandbox |
|
||||
|
||||
**Release decisions.** John, 2026-10-23: **F13 to F17 are urgent and are fixed in 8.3.0**, as
|
||||
"next release" cases (case numbers to come). Hosted and self-hosted perpetual installations
|
||||
matter equally; how many use customer contact logins is unknown, so every installation is
|
||||
assumed to. **F5, F1 and F18 are fixed in 8.3.0 as well** (John, 2026-10-23). John: "I won't
|
||||
release until it's secure, all holes must be plugged": 8.3.0 waits for the findings of every
|
||||
pass to be fixed or explicitly accepted by John.
|
||||
|
||||
### F5 2FA step skips the password (pass 2, confirmed on the local server)
|
||||
|
||||
`AuthController.TfaAuthenticate` (`POST auth/tfa-authenticate`, anonymous) finds the user
|
||||
only by `TempToken == pin.TempToken` (plus active, login allowed, 2FA on), then checks the
|
||||
6-digit code. The temp token is the only proof that the password was given in step one, but:
|
||||
|
||||
- after a successful 2FA login it is set to `""` (`AuthController.cs:193`), so sending
|
||||
`"tempToken": ""` matches every 2FA user who has completed a 2FA login;
|
||||
- a user who has just turned 2FA on has it `null`; sending no temp token matches them;
|
||||
- `TempToken` is not required in `TFAPinParam`, and a wrong code does not clear it.
|
||||
|
||||
So anyone, with no login and no password, can send codes to this route and log in as
|
||||
whichever 2FA user the query returns first. TOTP accepts 3 codes at any moment
|
||||
(TwoFactorAuth.Net default discrepancy 1), the only throttle is the 3 s failure delay,
|
||||
which parallel requests get around, and there is no lockout (F6) and no nginx rate limit on
|
||||
hosted droplets. At a few hundred guesses per second that is, on average, well under an
|
||||
hour; even at 20 per second, a few hours. Users who turn 2FA on are the more privileged
|
||||
ones, so 2FA makes those accounts weaker than a password alone.
|
||||
|
||||
**Proof** (local dev server, seeded `Inventory` user, 2FA turned off again afterwards):
|
||||
2FA just turned on, no password, temp token omitted, correct code: 200 and a token for
|
||||
Inventory. After one normal 2FA login, no password, temp token `""`, correct code: 200 and a
|
||||
token. Wrong code: 401. (Script kept outside the repo; the test needs the user's code, which
|
||||
an attacker guesses.)
|
||||
|
||||
**Fix** (proposed for 8.3.0): reject a null or empty temp token before the query; clear the
|
||||
temp token (set it to null) on success and on every failed code, so each guess needs the
|
||||
password again; optionally expire the temp token after a few minutes. Small change in one
|
||||
method; client unaffected (it always sends the `tt` it received).
|
||||
|
||||
### F1 Placeholder JWT secret (from the 8.3.0 installer test; confirmed in pass 2)
|
||||
|
||||
`dist/assets/lan-install-config.json` and `dist/assets/linux-server/config.json` ship
|
||||
`AYANOVA_JWT_SECRET` set to a placeholder that is also printed in the manual. The manual
|
||||
tells users to change it (Linux: "optional but recommended"). An installation that keeps it
|
||||
signs logins with a publicly known secret.
|
||||
|
||||
**Pass 2: the latest-token check does not save it.** Tokens are deterministic: the payload is
|
||||
only `exp` (login time + 5 days, in seconds), `iss` and `id`, with no random part
|
||||
(`AuthController.cs:280-291`); two logins in the same second give identical tokens. So with
|
||||
the secret, someone with no login can rebuild a user's *current* token by trying each second
|
||||
of the last 5 days: at most 432,000 requests per user, each rejected at once (no delay). It
|
||||
works for any user who logged in in the last 5 days, SuperUser included. **Proof**: with the
|
||||
local dev secret, a forged token for `Inventory` was accepted after 6 tries and was identical
|
||||
to the real one. In addition, routes ending in `/auth` (F8) accept a forged token without
|
||||
any search, and behind a reverse proxy on the same machine that does not set
|
||||
`X-Forwarded-For`, a forged token with an `rpl` claim and `Host: 127.0.0.1` skips the check
|
||||
too (`IsLocalExtension`: remote address equals local address).
|
||||
|
||||
Hosted droplets set up with the current script are not affected (random secret since
|
||||
`64d8f62`); **droplets created before that: to check (John)**. Self-hosted IIS and Linux
|
||||
servers that are reachable from the internet and kept the placeholder are fully open.
|
||||
|
||||
Fix options: treat the placeholder (and any secret shorter than 32 characters) as unset, so
|
||||
a random secret is used per boot, and log a warning (users would log in again after each
|
||||
restart); or generate a random secret at install time (Windows installer, Linux packages);
|
||||
or refuse to boot. Adding a random value (`jti`) to the token is cheap hardening as well.
|
||||
|
||||
### F6 No lockout or rate limit on login and 2FA (pass 2)
|
||||
|
||||
`POST auth` and `POST auth/tfa-authenticate` delay 3 s on failure but count nothing: no
|
||||
per-user or per-address lockout, and parallel requests are not slowed. Hosted nginx has no
|
||||
`limit_req`. There is no password rule beyond "not empty" (seeded trial users use the login
|
||||
as password). Fix options: count failures per login and per temp token with a short lockout;
|
||||
`limit_req` on `/api/v8/auth` in the hosted nginx template.
|
||||
|
||||
**Fixed (2026-10-24).** 2FA: case 4710 (5 wrong codes clear the temporary token). Password login: case 4726, `util/LoginThrottle.cs` in AyaNova and Sockeye (Sockeye's login is public too; only SSH to its droplet is limited to the office address). Failures are counted in memory: 10 per login name or 50 per client address within 15 minutes, then attempts are refused with 429 "Too many failed login attempts" for the rest of the window, without checking the password; a correct password clears the login's count; unknown logins are counted the same way. Loopback is not counted per address (the hosted nginx and IIS pass the real client address through the forwarded headers, which are trusted from loopback only). Each lockout is logged as a warning. Accepted: an attacker can keep a known login locked by failing 10 times every 15 minutes. Proved in both servers, the per-address limit through `X-Forwarded-For` with a documentation address; test `LoginLockout`. The password rule and nginx `limit_req` are in Deferred in `NET10-PORT.md`.
|
||||
|
||||
**Follow-up, case 4732 (2026-10-25).** The per-address count relies on `UseForwardedHeaders`, which by default trusts only a loopback proxy. Behind a proxy on another computer every user appeared as the proxy's address and shared one count of 50, so one person could lock everyone out. New setting `AYANOVA_TRUSTED_PROXIES` (addresses or CIDR networks) adds trusted proxies and follows a chain of them; when `X-Forwarded-For` arrives from a proxy that is not trusted, the log warns once per server start. Default behaviour unchanged.
|
||||
|
||||
### F7 Password change does not end sessions; download key loses its expiry (pass 2)
|
||||
|
||||
`UserBiz.ChangePasswordAsync` (used by change-password and reset-password) does not replace
|
||||
`CurrentAuthToken`, so a session someone else holds keeps working up to 5 days after the
|
||||
owner changes the password. It also sets `DlKeyExpire = null`, apparently meant to be
|
||||
`PasswordResetCodeExpire`; with a null expiry `ValidateDownloadTokenAndReturnUserAsync`
|
||||
never treats the key as expired, until the next login replaces it. Setting `AllowLogin` to
|
||||
false does not end a session either (only deactivating does). Fix: on password change, set
|
||||
`CurrentAuthToken` and `DlKey` to new random values and clear `PasswordResetCodeExpire`. **Fixed in case 4725 (2026-10-24):** a password set by someone else (reset code, administrator on the user form) and unticking Allow login replace `CurrentAuthToken` and `DlKey` (key expired); changing one's own password keeps the caller's session, which is the only valid one anyway (one session per user). `PasswordResetCodeExpire` is cleared instead of `DlKeyExpire`. Test `Sessions` (fails on the old code).
|
||||
|
||||
### F8 Latest-token check bypassed by the URL suffix (pass 2, confirmed)
|
||||
|
||||
The middleware skips the `CurrentAuthToken` comparison when the path ends with `/auth` or
|
||||
`notify/hello` (`Startup.cs:409-413`, `EndsWith`), meant for the login and pre-login routes.
|
||||
Any route whose last segment is free text can be called with a replaced or revoked token
|
||||
(e.g. `tag-list/batch-add/auth`, `form-custom/auth`). Confirmed: a replaced token got E2004
|
||||
on `form-field-reference/Customer` and passed the middleware on `form-field-reference/auth`.
|
||||
The middleware also does not check `Active`. Impact is small on its own (only those routes,
|
||||
with the user's current roles), larger with F1. Fix: compare the exact route
|
||||
(`/api/v8/auth`, `/api/v8/notify/hello`), or skip the check by endpoint metadata
|
||||
(`[AllowAnonymous]`) instead of by path. **Fixed in case 4725 (2026-10-24):** the exemption compares the matched route exactly (`POST auth`, `GET notify/hello`, through `CustomerContactRoutes.RouteKey`); the middleware also refuses inactive users and users who may not log in. Test `Sessions.ReplacedTokenRefusedOnRouteEndingInAuth` (old code: passed the middleware, 404 from the action).
|
||||
|
||||
### F9 Default SuperUser password, advertised anonymously (pass 2)
|
||||
|
||||
A new database has SuperUser `superuser` / `l3tm3in` (`PrimeData.cs`). Anonymous
|
||||
`GET notify/hello` returns `sudf: true` while that is still the password, and the login form
|
||||
then fills it in. Anyone can therefore check an internet-facing server for it in one request
|
||||
and log in as SuperUser. Each call also runs a PBKDF2 hash, a cheap way to load the server.
|
||||
Whether hosted droplets set their own SuperUser password at setup: to check in pass 10. **Pass 10:** they do not. The setup script generates the database password and JWT secret but not a SuperUser password, so every new hosted droplet starts internet-facing with `l3tm3in` until someone changes it. Fix in the script: generate one, set it once with `AYANOVA_SET_SUPERUSER_PW`, and write it to `/root/ayanova-secrets.txt` with the others. **John, 2026-10-23:** the published password is deliberate while a server is in trial mode, for easy evaluation; once licensed for production it must be changed on first login. Checked: that rule is enforced by the client only (`api/initialize.js` sends the user to the password page when `knownPassword` and the licence status is ActivePurchased); the server still accepts `superuser`/`l3tm3in` on a licensed server (API logins, or a user who navigates away) and `notify/hello` keeps reporting it. Fix that keeps the trial design: on a purchased licence, have the server refuse a login with the default SuperUser credentials (or allow it only to change the password), and stop returning `sudf` outside trial mode.
|
||||
Fix options: only return `sudf` from local or LAN addresses, or force a password change on
|
||||
first SuperUser login; at least warn in the UI and the manual.
|
||||
|
||||
**Fixed in case 4725 (2026-10-24), John's option A:** with a purchased licence (active or expired), `notify/hello` no longer returns `sudf`, and a SuperUser session with the default credentials may only use the routes the change password page needs (`SuperUserDefaultPasswordRoutes`, checked in the token middleware; the default-credentials result is kept per stored hash, so no extra hashing per request). Trial and unlicensed servers unchanged, as designed. Proved locally with a temporary, uncommitted purchased-licence override: other screens 403 until the password was changed, then normal (John's click-through). Hosted setup script still does not set a SuperUser password: accepted for trial mode.
|
||||
|
||||
### F10 Anonymous licence-agreement consent (pass 2)
|
||||
|
||||
`POST license/lc` with body `"iaccepttheagreement"` records the EULA consent; the comment says
|
||||
"SuperUser only", but the route is anonymous. Legal rather than technical: the consent record
|
||||
does not prove who accepted. Fix: require SuperUser.
|
||||
**John, 2026-10-24: accepted as is** (login is refused until consent is given, so consent can't require a login; requiring credentials in the request would need a client change). To be checked from a legal standpoint.
|
||||
**John, 2026-10-26: moved out of case 4727 into case 4736, after 8.3.0.**
|
||||
|
||||
### F11 Anonymous API root page discloses licence details (pass 2)
|
||||
|
||||
`GET /api/v8/` returns an HTML page with the exact version, "Registered to", licence type,
|
||||
expiry dates and licensed features; unlicensed, the server DB ID. Helps an attacker pick
|
||||
targets by version. Fix: show it only to logged-in users, or show the version only.
|
||||
**Fixed in case 4727, step 2 (2026-10-24):** the page shows the version and the links only; the licence block is gone and the support link no longer carries the DB ID and licensee (it did, unlicensed or not). The DB ID stays available to logged-in users (licence form, About form support link). Test `ApiRoute.ServerApiRootPageHasNoDbIdOrLicence`. The manual's screenshot of this page (`api-console.md`) still shows the old licence block.
|
||||
|
||||
### F12 Password hashing strength (pass 2)
|
||||
|
||||
PBKDF2-HMAC-SHA512, 10,000 iterations, 32-byte random salt (`Hasher.cs`). Current guidance
|
||||
(OWASP) is 210,000 iterations for SHA-512. Only matters if the database or a backup leaks.
|
||||
Raising it needs re-hashing at next login (store the iteration count). Roadmap item, not 8.3.0.
|
||||
**2026-10-24: later case, not 8.3.0** (John left it to the review). It only matters if the database or a backup leaks (online guessing is limited by case 4726); 210,000 iterations make each login, change and reset about 20 times more work, to be measured on a small droplet; it needs a schema change (iteration count per user) and re-hashing at each user's next login.
|
||||
|
||||
### Pass 3 overview: customer contact isolation
|
||||
|
||||
Customer and head-office contacts are users with `UserType` Customer/HeadOffice and the
|
||||
`Customer` role (seeded contacts: `AuthorizationRoles.Customer`). Isolation to their own
|
||||
customer is enforced **only in data lists**: `DataListController` allows them two lists
|
||||
(`CustomerServiceRequestDataList`, `CustomerWorkOrderDataList`) and sets the customer
|
||||
filter itself. Everywhere else the checks are role sets designed for staff, and
|
||||
`AuthorizationRoles.All` includes `Customer` and `CustomerRestricted`: `Select` is `All` on
|
||||
almost every type (Customer, User, Vendor, Part, Unit, WorkOrder…), `ReadFullRecord` is `All`
|
||||
on Report, TaskGroup, QuoteStatus, and the `Customer` role can change any
|
||||
CustomerServiceRequest. The customer-specific rights helper
|
||||
(`UserBiz.CustomerUserEffectiveRightsAsync`) answers "is this feature on for this contact
|
||||
and these tags", never "does this record belong to the contact's customer".
|
||||
|
||||
All proofs below: local dev server, seeded contact `Customer` (customer id 1), read-only
|
||||
requests; seeded data is fake.
|
||||
|
||||
**Fix direction** (one case, possibly in steps): deny customer-type users by default, with an
|
||||
allowlist of the routes the customer UI actually calls (to be listed from the client), as a
|
||||
filter or in the token middleware, where `AY_USER_TYPE` is already set; then add the
|
||||
ownership check in each allowed route that takes an id (work order customer view, customer
|
||||
report, attachments, CSR). This also covers routes not yet reviewed.
|
||||
|
||||
### F13 Any customer's work order by id (pass 3, confirmed)
|
||||
|
||||
`GET workorder/customer/{id}` (`WorkOrderBiz.WorkOrderGetForCustomerAsync`) loads the work
|
||||
order by id and never compares its `CustomerId` with the contact's. It returns serial, service
|
||||
date, customer name, customer reference number, contact name, invoice number, and the wiki
|
||||
when tag rules allow it. **Proof**: the contact of customer 1 read work order 1 of customer 12
|
||||
(200, with that customer's name). A staff user calling it gets a 500 (the helper throws for
|
||||
non-customer users). The same tag-only rights, without ownership, also gate (code reading):
|
||||
|
||||
- the customer work-order report (`ReportBiz`, report id `-100`): takes `SelectedRowIds[0]`,
|
||||
gets the effective report from that work order's tags, then renders with the role check
|
||||
skipped (`RequestIsCustomerWorkOrderReport`), so a contact can render the configured
|
||||
customer report for any work order (pass 4 to confirm);
|
||||
- work-order attachments (`attachment/list`, `attachment/download/{id}` for customer users):
|
||||
`ThisWOCanAttachments` from tags only.
|
||||
|
||||
These need the customer work-order features turned on in global settings (common where the
|
||||
customer portal is used); the customer view route does not, it always answers.
|
||||
|
||||
### F14 Customer service requests across customers (pass 3, confirmed for reading)
|
||||
|
||||
CSR `Change` includes `Customer`, and `HasReadFullRole` counts `Change` as read.
|
||||
`CustomerServiceRequestBiz` never checks `CustomerId` against the contact (create, get, put,
|
||||
delete). **Proof**: the contact read CSR 1 of customer 16 (200, with its text). Change, delete
|
||||
and create for another customer were not tested (they write data) but follow from the same
|
||||
code. Search also returns CSRs of all customers (it filters by role only). Pass 9: `unit/customerunit` (`UnitBiz.CustomerCreateAsync`) also takes `CustomerId` from the request, so a contact can add units to any customer; same fix (force the contact's customer).
|
||||
|
||||
### F15 Pick lists expose the whole customer list (pass 3, confirmed)
|
||||
|
||||
`POST pick-list/list` checks only `Select`, which the `Customer` role has for customers, head
|
||||
offices, vendors, users, parts, units, contracts, projects, rates and more. The caller also
|
||||
sends its own `template`, which chooses the returned columns. **Proof**: the contact got all
|
||||
70 seeded customers with phone and email. `pick-list/single` works the same way. A
|
||||
customer's contact can therefore download the company's customer list with contact details.
|
||||
Fix: no `Select` for customer roles on these types (keep what the customer UI needs, likely
|
||||
units of its own customer) and ignore `template` from customer-type users.
|
||||
|
||||
### F16 Service schedule open to everyone logged in (pass 3, confirmed)
|
||||
|
||||
`POST schedule/svc` and `POST schedule/user` have no role check. **Proof**: the contact got
|
||||
1,257 schedule items, each "work order number + customer name" with times and the assigned
|
||||
technicians. Fix: require read rights on `WorkOrderItemScheduledUser` (or `WorkOrder`) and
|
||||
refuse customer-type users. (`schedule/personal` is the caller's own schedule.)
|
||||
|
||||
### F17 Notification subscription for someone else (pass 3, candidate)
|
||||
|
||||
`NotifySubscriptionBiz.CreateAsync` and `PutAsync` keep the `UserId` sent by the client, and
|
||||
validation checks the event type against *that* user's type, not the caller's. A subscription
|
||||
has its own `DeliveryAddress` and can attach a report (`LinkReportId`). So, by code, a
|
||||
customer contact could create a subscription with a staff user's id, a staff event type and
|
||||
its own email address, and receive staff notifications, possibly with reports attached. Not
|
||||
tested live because it sends real email. Fix: force `UserId` to the caller unless the caller
|
||||
has user-admin rights. Pass 8 to confirm how delivery checks rights. **Pass 8 (code):** confirmed, and it goes further: `NotifyEventHelper` matches object events (created, modified, deleted, age) to subscriptions by record type only, without checking that the subscriber may read that type, and `CoreJobNotify` sends to the subscription's `DeliveryAddress`. A user notification email carries the record type, its name and a link (no data, no report: report attachments exist only on customer notification subscriptions, which staff create). So a contact, or a staff user without rights on a type, can get a running feed of record names (new customers, work orders, ...) at any address. Re-rated Medium. Fix as above, plus a read-rights check on the subscriber when an object event is matched.
|
||||
|
||||
### F18 Pick-list search text in SQL (pass 3; code reading completed in pass 6)
|
||||
|
||||
`PickListSqlBuilder.Build` puts the search text and the tag search text into `like` fragments
|
||||
by string interpolation, and no escaping was found on the way from
|
||||
`PickListController.PostList` through `PickListBiz.GetPickListAsync`. The route is open to
|
||||
every logged-in user, customer contacts included. Data-list text filters escape quotes since
|
||||
case 4676; pick lists were not covered. Deliberately not tested live. Fix: pass the search
|
||||
texts as SQL parameters (preferred) or escape them as the data-list builder does; review the
|
||||
rest of the pick-list builder in pass 6. **Pass 6:** the search text (`autoCompleteQuery`) and the tag search text (`tagSpecificQuery`) are the only free-text inputs of the pick-list builder, and neither is escaped on any path; template fields map to column definitions, ids are `long`, variants are parsed as numbers or matched to fixed strings. Status: confirmed by code.
|
||||
|
||||
|
||||
### F19 Report template import open to every login (pass 4, confirmed)
|
||||
|
||||
`POST report/upload` (`ReportController.UploadAsync`) has `[Authorize]` only, and
|
||||
`ReportBiz.ImportAsync` checks nothing either; creating or editing a template through the
|
||||
normal routes needs `Change` on `Report` (BizAdmin, BizAdminRestricted). A template holds
|
||||
JavaScript (`JsPrerender`, `JsHelpers`) and a Handlebars template, which `ReportBiz` loads into
|
||||
the headless browser on the server at render time. **Confirmed**: the seeded contact
|
||||
`Customer` imported a copy of a stock template (202, new report 104, deleted afterwards).
|
||||
|
||||
Consequences (from code; the rendering side was deliberately not tested):
|
||||
- `report/render-job` requires read rights on `Report` (everyone) and on the template's
|
||||
record type, which the uploader chooses (a contact can read CSRs), so any login can run
|
||||
its own JavaScript in the server's browser: it can reach the local API and any address the
|
||||
server can reach (internal network, cloud metadata endpoints) and put the results into the
|
||||
PDF it downloads.
|
||||
- An imported template also appears in staff report lists; when staff or SuperUser render it,
|
||||
it runs with their token (see F20).
|
||||
|
||||
Fix: in `UploadAsync`, require `HasCreateRole(Report)` like `POST report`; F20's fixes limit
|
||||
what a template can do even for legitimate authors.
|
||||
|
||||
### F20 What a report template can reach (pass 4, candidate)
|
||||
|
||||
The page gets the renderer's own bearer token in `AYMETA.ayClientMetaData.Authorization` (for
|
||||
`ayGetFromAPI`/`ayPostToAPI`/`ayPutToAPI`), and nothing restricts the page's network access
|
||||
(no request interception in `ReportBiz`). So a template's script can send the token of
|
||||
whoever renders it to any outside address; that token is the user's current session token,
|
||||
usable from anywhere. Customer notifications with an attached report
|
||||
(`CoreJobCustomerNotify`) render with `AuthController.GenRpt`: a SuperUser (id 1) token with an
|
||||
`rpl` claim, accepted from 127.0.0.1 for the render timeout. A template author (including the
|
||||
"restricted" `BizAdminRestricted` role) can therefore act as SuperUser, or as any colleague who
|
||||
runs the report. Fix options: intercept page requests (PuppeteerSharp
|
||||
`SetRequestInterceptionAsync`) and allow only the local API URL and `data:`/`blob:`; render
|
||||
notification reports as a dedicated low-rights identity rather than SuperUser; consider a
|
||||
short-lived token limited to read routes for template API calls.
|
||||
|
||||
**Fixed in case 4719 (2026-10-25).** The report page holds only placeholders for the renderer's token and download key (`Bearer ay-report-render-token`, `ay-report-render-download-key`); request interception (`util/ReportApiRelay.cs`, from case 4632, now in every kind of install) puts the real values only on the page's requests to `ayApiUrl`, so a template that sends them elsewhere sends a placeholder (stock template "z_user logged in ... ayClientMetaData" printed the real token and download key into the PDF until now). Report templates are changeable only by BizAdmin (BizAdminRestricted had change rights, against the roles page), so only someone who can already change users' roles authors what runs with other people's rights, including SuperUser for notification reports, which keep their rights (customers write from reports). Outside addresses stay reachable (customer templates may load their own images or fonts); a template author can still send out data they can read, which is no escalation. Tests `ReportEditRights`, `ReportRenderCredentials` (before: the page saw the real token and download key); 22 stock reports unchanged locally; IIS VM user and notification reports.
|
||||
|
||||
### F21 File names from the URL in download routes (pass 4, candidate, code reading)
|
||||
|
||||
`report/download/{fileName}` and `export/download/{fileName}` accept any user's download key
|
||||
and serve `FileUtil.GetFullPathForTemporaryFile(fileName)`, which is
|
||||
`Path.Combine(TemporaryFilesFolder, fileName)` with no check. On Windows, a backslash in the
|
||||
route value is not a separator for routing but is one for `Path.Combine`, and an absolute
|
||||
name replaces the folder entirely; the result is passed to `PhysicalFile`. Any login
|
||||
(customer contacts included) could then read files outside the temp folder, such as
|
||||
`config.json` (database password, JWT secret). Linux is likely not affected (`/` stays encoded
|
||||
as `%2F` in route values); IIS request filtering may block some forms. Not tested live by
|
||||
choice. The same unchecked pattern is in `log-file/{logname}`, `log-file/download/{logname}`
|
||||
and the backup download route (ops roles; pass 5 confirmed: `LogFilesController` `GetLog` and `DownloadLog` need `LogFile` read, i.e. OpsAdmin or OpsAdminRestricted; `BackupController.DownloadAsync` needs `Backup` change, i.e. OpsAdmin; backup *delete* is safe, `FileUtil.EraseBackupFile` already strips directories with `Path.GetFileName`, the pattern the other routes should share). Fix: accept only a bare file
|
||||
name (`Path.GetFileName(name) == name`, no `..`), and check the resolved full path is inside
|
||||
the folder, in one helper used by every such route.
|
||||
|
||||
### F22 Minor report and job gaps (pass 4)
|
||||
|
||||
`report/export/{id}` gives any report template to any valid download key (templates are not
|
||||
secret, but staff-only templates go to customer contacts). `job-operations/status|progress|
|
||||
logs/{gid}` and `report/request-cancel/{gid}` check no owner; job ids are random GUIDs, so
|
||||
this needs a leaked id. The browser runs with `--no-sandbox` (Chrome's own sandbox off), so a
|
||||
browser bug reached through a template is not contained; Linux runs as `www-data`, Windows LAN
|
||||
as the logged-in user. Fix: owner check on job routes; consider enabling the sandbox where
|
||||
the platform allows.
|
||||
**John, 2026-10-24:** owner check and template export in case 4727; Chrome's sandbox **accepted as off** (it often fails on Linux servers and can't run as root). The systemd options in F38 would contain the browser as a child of the service on hosted droplets.
|
||||
**Case 4727, step 4 (2026-10-24):** `report/export/{id}` gives a template only to a download-key user whose (effective) roles include one of the template's Roles, or who may edit templates; test `TemplateExportRights` (a user with no roles got any template on the old code). Job owner check **accepted, John 2026-10-24**: jobs record no owner, so it needs a schema change and changes at the 13 places that create jobs; job ids are random GUIDs and the routes return status and log only. A possible later case.
|
||||
|
||||
### F23 Logo upload: any content type, served inline (pass 5, from code)
|
||||
|
||||
`LogoController.UploadAsync` stores the bytes and the content type the browser sent
|
||||
(`file.ContentType`), with no check of either; `DownloadLogo` (anonymous, `GET logo/{size}`)
|
||||
returns them inline with that type from the AyaNova origin. A file uploaded as HTML (or SVG
|
||||
with script) therefore runs as a page of the AyaNova site when someone opens its URL, and the
|
||||
client keeps its session in `localStorage` (`vuex-persistedstate`, key `AyaNova`), which such a
|
||||
page can read. Upload and delete check `HasReadFullRole(Global)`: BizAdmin, and
|
||||
BizAdminRestricted, whose rights on Global are meant to be read-only. Fix: require
|
||||
`HasModifyRole(Global)`; accept only PNG, JPEG, GIF, WebP (check the bytes, not the header;
|
||||
no SVG); serve with `X-Content-Type-Options: nosniff` and a restrictive
|
||||
`Content-Security-Policy` (pass 9 looks at headers generally).
|
||||
|
||||
**Fixed in case 4727, step 1 (2026-10-24):** upload and delete need `HasModifyRole(Global)` (BizAdmin); the type is taken from the bytes (PNG, JPEG, GIF, WebP, SVG; anything else refused, the claimed and stored types are ignored); downloads send `nosniff` and `Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; sandbox`. SVG is kept (the client accepts any image, so existing customers may have one): the sandbox stops its script when it is opened by URL, `<img>` use is unaffected. Test `LogoSafety`; an SVG with a script was uploaded, served sandboxed, and the original logo restored.
|
||||
|
||||
### F24 Translation import without a role check (pass 5, from code)
|
||||
|
||||
`TranslationController.UploadAsync` (`POST translation/upload`) and `TranslationBiz.ImportAsync`
|
||||
check no role, the same shape as F19. Any login, customer contacts included, can add
|
||||
translations (up to 15 MiB each; imported as non-stock under a unique name). They affect
|
||||
only users who select them, unless an administrator makes one the default; whether
|
||||
translated text is ever rendered as HTML in the client is a pass 9 question. Fix: require
|
||||
`HasCreateRole(Translation)`, like the duplicate route.
|
||||
|
||||
### F25 Attachment and export gaps (pass 5, from code)
|
||||
|
||||
- `AttachmentController.UploadAsync` streams the whole upload to disk (limit 10 GiB) before
|
||||
it checks rights on the target record, then deletes it on 403: any login can make the
|
||||
server write large files repeatedly. Check rights before reading the body, or lower the
|
||||
limit for users without rights.
|
||||
- The form field `OverrideUserId` is accepted from anyone and recorded as "attached by":
|
||||
limit it to the import path that needs it, or to SuperUser.
|
||||
- `export/download/{fileName}` accepts any user's download key; the name is
|
||||
`{type}-{yyyyMMddHHmmssfff}.json` and files are deleted after about 15 minutes, so guessing
|
||||
another user's export is impractical, but the route should still check that the caller
|
||||
made it.
|
||||
- Customer contacts can attach files to any customer's service request (part of F14; fixed
|
||||
by the same ownership check).
|
||||
|
||||
**John, 2026-10-24:** `OverrideUserId` and the export download check in case 4727; the upload written to disk before the rights check is **accepted** (since case 4712 every login that reaches it is staff; a proper fix restructures the upload, a possible later case).
|
||||
**Case 4727, step 5 (2026-10-24):** `OverrideUserId` is used only from SuperUser or a user with change rights on User (the v7 migration's need), otherwise ignored (proved: a Service user's attachment claiming SuperUser was recorded as SuperUser's on the old code, as the Service user's now). Export files are named `{type}-u{userId}-{date}.json` and `export/download` serves only a `.json` name carrying the download-key user's own id, so it no longer serves other temporary files either; test `ExportOwnFileOnly` (another user's key downloaded the file on the old code).
|
||||
|
||||
OK in this pass: attachments are served as downloads (`Content-Disposition: attachment`), stored
|
||||
under content-hash names in the attachments folder; batch move, batch delete and maintenance
|
||||
check rights; data import (`POST import`) checks create rights per type; there is no restore
|
||||
route (restore is a manual `pg_restore`, see the manual).
|
||||
|
||||
### F26 KPI `interval` in SQL (pass 6, from code)
|
||||
|
||||
Six KPIs (`WorkOrderCreatedCount`, `WorkOrderStatusCount`, `WorkOrderStatusPct`,
|
||||
`WorkOrderCompletedOnTimePct`, `WorkOrderItemLaborQuantitySummary`,
|
||||
`WorkOrderItemLaborQuantityEveryoneSummary`) read `interval` from the request
|
||||
(`options.Criteria["interval"]`, a string) and put it into the query as
|
||||
`DATE_TRUNC('{interval}', ...)` with no check. `POST kpi` has no role check of its own;
|
||||
`KPIFetcher` checks the KPI's `AllowedRoles`, which for the labour summary are Tech and
|
||||
TechRestricted, so the least privileged staff roles reach it (customer roles do not). A value
|
||||
that leaves the quotes changes the query, which can read or change anything in the database
|
||||
(password hashes, roles, licence). Fix: accept only the values the client sends (`day`, `week`,
|
||||
`month`, `quarter`, `year`; check `raven-client` for the exact list) and reject anything else,
|
||||
in one helper used by all six. The other KPI criteria are safe: `timeSpan` goes through the
|
||||
data-list date builder (fixed tokens, otherwise `DateTime.Parse`), tags through the tag
|
||||
helper (escaped), `wostatus` and `userid` are integers.
|
||||
|
||||
### Raw SQL: what was checked and found safe (pass 6)
|
||||
|
||||
- **Data-list filters** (`DataListSqlFilterCriteriaBuilder`): column keys are looked up in the
|
||||
list's field definitions (unknown keys throw); text, phone, email and URL values have quotes
|
||||
doubled; numbers, roles and ids are parsed (case 4676); bools become `true`/`false`; dates
|
||||
and relative-date tokens are parsed; durations are rebuilt from parsed integers; tags are
|
||||
normalised and quote-doubled.
|
||||
- **Data-list sort and columns**: keys looked up in the field definitions; direction becomes
|
||||
`ASC`/`DESC`.
|
||||
- **`ClientCriteria`**: every list that reads it (51) turns it into a typed filter option, so it
|
||||
goes through the filter builder above; saved filters too.
|
||||
- **Search**: the word breaker keeps only letters, digits and `%`, so terms cannot contain a
|
||||
quote. Safe by construction, but fragile: parameters would not depend on the tokenizer.
|
||||
- **Customer notification "who" query, schedule, attachments list, name fetcher, unit and
|
||||
customer queries, accounting list, server metrics, job and sweeper queries**: typed values
|
||||
or fixed SQL; schedule uses EF.
|
||||
- `DbUtil` and `AySchema` build SQL from configuration (database name), not from requests.
|
||||
|
||||
Hardening, not a finding: hand-built SQL with string escaping is spread over the builders.
|
||||
Moving free-text values to `NpgsqlParameter`s (pick lists, data-list text filters, search,
|
||||
tags) would remove the whole class of bug; at least F18 and F26 should use parameters or an
|
||||
allowlist rather than escaping.
|
||||
|
||||
### F27 Restricted roles bypass work-order item filtering (pass 7, confirmed)
|
||||
|
||||
`WorkOrderBiz.WorkOrderGetAsync` removes, for TechRestricted, SubContractor and
|
||||
SubContractorRestricted users, every item they are not scheduled on, other users' labour,
|
||||
travel and scheduling, and (by role) parts, units, expenses, loans and outside services.
|
||||
`GetReportData` (also used by `GetExportData`) calls `WorkOrderGetPartialAsync`, which applies
|
||||
that filtering only when the requested type is `WorkOrder`; for the child types (item,
|
||||
labour, expense, part, ...) it loads the parent header and the whole item, with every
|
||||
descendant when `IncludeWoItemDescendants` is set by the caller, and filters nothing. These
|
||||
roles have change (so read) rights on the child types they work with, so `report/data`,
|
||||
`report/render-job` and `export/render` accept them. **Proof**: on work order 1, seeded
|
||||
`SubContractorRestricted` got 0 items from `workorder/1` (correct, not scheduled), and from
|
||||
`report/data` with a labour id and descendants on got the item with another user's labour,
|
||||
the scheduling and 2 parts (a role that should see no parts at all). Fix: apply the same
|
||||
restricted-user filtering in `WorkOrderGetPartialAsync` (one shared method used by both
|
||||
paths), and refuse child-type report and export requests for items the user is not scheduled
|
||||
on. Check `QuoteBiz` and `PMBiz` for the same split (they also reference restricted users).
|
||||
|
||||
### F28 Contact logins with staff roles (pass 7, candidate, code reading)
|
||||
|
||||
`UserBiz` lets anyone with modify (create) rights on `Customer` create and edit customer contact
|
||||
users (`IsOutsideCustomerContactTypeUser`), and `BizRoles` gives `Customer` change rights to
|
||||
Service, Sales and Accounting. Changing a user between inside and outside types needs rights
|
||||
on both User and Customer (`UserBiz.cs:803`), but nothing limits the **roles** a contact user may
|
||||
hold: `ValidateAsync` checks only that the roles value is valid. Authorisation is by role
|
||||
everywhere, so a contact with BizAdmin or OpsAdmin roles is an administrator. A Sales, Service
|
||||
or Accounting user could therefore create a contact login with administrator roles and a
|
||||
password of their choosing. Not tested live. The F13 to F17 allowlist would limit what a
|
||||
customer-type login can call, but the roles must still be constrained. Fix: in
|
||||
`UserBiz.ValidateAsync`, allow only `Customer` and `CustomerRestricted` roles on Customer and
|
||||
HeadOffice users (and only inside roles on inside users); reject anything else on create and
|
||||
update.
|
||||
|
||||
### F29 `server-info` to every login (pass 7)
|
||||
|
||||
`GET server-info` has no role check and returns the version, schema, server time zone, the
|
||||
server DB ID and the licence as JSON, to any login including customer contacts. The about
|
||||
page needs some of it; contacts need little or none. Whether the DB ID matters to Sockeye
|
||||
(licence fetch, trial requests) is checked in pass 11. Fix: return the DB ID and licence only
|
||||
to staff, or only to ops and admin roles.
|
||||
**Fixed in case 4727, step 3 (2026-10-24):** `server-info` returns the DB ID and licence, and `global-biz-setting/client` the DB ID and licensee, only to Service and NotService users (SuperUser is NotService), the same rule the About form already used to show them; customer contacts and subcontractors get empty values. Test `ApiRoute.DbIdAndLicenceForStaffOnly` (a contact received the DB ID on the old code).
|
||||
|
||||
### Pass 7: routes without a role check that were found OK
|
||||
|
||||
`DELETE notify/{id}` (owner checked), `notify/new-count`, `notify/app-notifications`,
|
||||
`notify-subscription/list` (the caller's own), data-list column views (validation refuses
|
||||
another user's id), saved filters (own or public), `user-option/{id}` (own unless admin),
|
||||
`auth/change-password`, `auth/totp*` (own), `POST kpi` (each KPI checks its own roles; but see
|
||||
F26), `POST search` (results filtered by read rights per type), `GET user/amsu` (returns only
|
||||
whether the caller is SuperUser), enum lists, form field references, translations (read),
|
||||
tag lists, role lists, build mode, server state, `global-biz-setting/client` (display
|
||||
settings only). Already covered elsewhere: `job-operations/*` (F22), `schedule/*` (F16),
|
||||
`workorder/customer/{id}` (F13), `report/upload` (F19), `translation/upload` (F24),
|
||||
`notify/direct-smtp` and `direct-message` (F3, F4).
|
||||
|
||||
### F30 SMTP certificate not checked (pass 8, from code)
|
||||
|
||||
`Mailer.SendEmailAsync` sets `client.ServerCertificateValidationCallback = (s, c, h, e) => true`,
|
||||
so any certificate is accepted. Anyone who can intercept the connection (a hostile network,
|
||||
DNS) can pose as the mail server and collect the SMTP account and password and every message.
|
||||
Also, with security set to none, MailKit's `ConnectAsync(host, port, false)` uses STARTTLS only
|
||||
if offered, which an attacker can suppress. Fix: remove the callback (validate normally); if a
|
||||
customer needs a self-signed mail server, make that an explicit setting. **Fixed in case 4724 (2026-10-24):** the callback is set only when the new config setting `AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE` is true (boot warning when it is; shown in server info; manual page `ops-config-smtp-accept-any-certificate.md`). Revocation checking is off (`CheckCertificateRevocation = false`) so an unreachable revocation server can't stop email; trust and name are checked. Proved locally against a TLS listener with a self-signed certificate: refused ("root certificate which is not trusted") without the setting, accepted with it. The STARTTLS-stripping point for security None stays as chosen by the customer; the security option mapping is in Deferred.
|
||||
|
||||
### F31 SMTP password returned to read-only roles (pass 8, from code)
|
||||
|
||||
`GET global-ops-notification-setting` returns the whole settings object, `SmtpPassword` included,
|
||||
to every role with read rights on `OpsNotificationSettings`: OpsAdminRestricted, BizAdmin and
|
||||
BizAdminRestricted, while only OpsAdmin may change it. Those users can then use the company's
|
||||
mail account directly. Fix: never send the password to the client (return a "set" flag; keep the
|
||||
stored value when the PUT leaves it empty). **Fixed in case 4724 (2026-10-24):** the GET returns `********` (entity detached first); a PUT with `********` or an empty value keeps the stored password. Test `SmtpPasswordHidden`; the stored password's fingerprint was unchanged after the round trip. Before the fix BizAdmin received the full password.
|
||||
|
||||
### F32 SMTP credentials in source (pass 8)
|
||||
|
||||
`models/GlobalOpsNotificationSettings.cs`, in the `#if (DEBUG)` constructor, sets a FastMail SMTP
|
||||
server, account and password; the same file is in `sockeye/server/models`. Release builds use
|
||||
placeholders, so shipped software is not affected, but the credentials are in both repositories
|
||||
and their history (contrary to the "secrets never go in a repo" rule), and debug builds send real
|
||||
email through that account (which explains report 81's real bounces in testing). Fix: rotate that
|
||||
FastMail app password now (John); remove the values from both files and read debug SMTP settings
|
||||
from an environment variable or user secrets. History need not be rewritten once the password is
|
||||
rotated. **John, 2026-10-23:** rotation planned once the old Sockeye server is retired after the
|
||||
migration; its own case.
|
||||
|
||||
OK in pass 8: SMTP settings change and the test-send route are OpsAdmin only; batch direct SMTP
|
||||
(`job-operations/batch-direct-smtp`) checks modify rights on the record type and sends only to the
|
||||
records' own addresses; notification emails are plain text built from record names (no HTML
|
||||
injection); the customer notification "who" query escapes tags.
|
||||
|
||||
### F33 Unescaped record text rendered as HTML in the client (pass 9, from code)
|
||||
|
||||
Of the 13 `v-html` uses in `raven-client`, these render text that users enter, without
|
||||
escaping it first:
|
||||
- `views/home-search.vue`: the search result excerpt (`item.info`, from `Search.GetInfoAsync`,
|
||||
which does not encode it), with the search terms highlighted. Searchable text includes
|
||||
fields customer contacts can write (service requests, units).
|
||||
- `components/work-order-address.vue`: addresses built by `formatAddress` from customer, head
|
||||
office and unit address fields.
|
||||
- `components/work-order-item-units.vue`, `quote-item-units.vue`, `pm-item-units.vue`: the
|
||||
warranty summary (vendor name, receipt number, work order list).
|
||||
- `components/gzconfirm.vue`: dialog messages, which are translation strings; with F24 any login
|
||||
can import a translation.
|
||||
|
||||
The session token is kept in `localStorage` (`vuex-persistedstate`), so script running in the
|
||||
page could act as the signed-in user. Fix: escape these values (an `escapeHtml` helper, or
|
||||
build the markup with Vue templates instead of strings); encode the excerpt on the server too;
|
||||
render translation strings as text. Safe: the wiki (`marked` then DOMPurify, see F34), the
|
||||
calendar summaries (Vuetify escapes event names), and the remaining sinks (fixed strings).
|
||||
A Content-Security-Policy (F35) would be a second layer.
|
||||
|
||||
### F34 Client libraries behind on security fixes (pass 9)
|
||||
|
||||
`npm audit` (read-only, lockfile) reports 550 advisories, almost all in build tools (webpack,
|
||||
Vue CLI, Babel, ESLint) that run only on the build machine and do not ship. Of the packages
|
||||
the app ships: **DOMPurify 2.3.8**, the wiki sanitiser, has sanitiser-bypass fixes in 2.4.x and
|
||||
2.5.x; **Vuetify 2.6.6** has XSS fixes in 2.6.10 (the last 2.x is 2.7.x); `marked` 1.2.9, `luxon`
|
||||
1.28.0 and `vue` 2.6.14 have only slow-regex (browser-side) advisories. Fix: raise DOMPurify to
|
||||
the latest 2.5.x and Vuetify to the latest 2.7.x, both on the same major version. This is a
|
||||
front-end dependency change, which CLAUDE.md rules out without John's decision; check that
|
||||
both still build with Node 12.22.9. **John, 2026-10-23: both upgrades approved for 8.3.0**, provided they do not conflict with the Node 12.22.9 pin or break the client. Checked (read-only `npm view`): DOMPurify 2.5.9 is the latest 2.x, no engine limit, same API; Vuetify 2.7.2 (latest 2.x, peer `vue ^2.6.4`) is the target, with **2.6.16** (last 2.6, has the 2.6.10 fix) as the fallback if webpack 4 on Node 12 cannot build 2.7. Prove by a full client build, the lint, and a smoke test of wiki, calendar and date pickers, then Cypress on `devtestn10`; `package.json` and `package-lock.json` change in their own commit.
|
||||
|
||||
### F35 Headers, CORS, Swagger, keys in URLs (pass 9)
|
||||
|
||||
- The server sets no security headers. Hosted droplets get HSTS, `nosniff`, `X-Frame-Options`
|
||||
and `Referrer-Policy` from the nginx template in the setup script; self-hosted IIS, Linux and
|
||||
Windows installs get none. No deployment sets a Content-Security-Policy.
|
||||
- CORS allows any origin, method and header. Impact is small because the token travels in the
|
||||
`Authorization` header (not a cookie), so another site cannot use a visitor's session.
|
||||
- Swagger UI and the full API description are public at `/api-docs`.
|
||||
- Download keys (`?t=`) are in URLs, so they reach proxy access logs and browser history;
|
||||
they last 5 days (F7 makes some last longer).
|
||||
|
||||
Fix: add the headers in the app (so every deployment gets them), with a CSP that allows the
|
||||
client's own scripts; limit CORS to the server's own origin unless API users need more;
|
||||
consider Swagger UI for logged-in users only.
|
||||
|
||||
**John, 2026-10-24:** the safe headers (`nosniff`, `X-Frame-Options`, `Referrer-Policy`) in case 4727. Content-Security-Policy for the client: its own later case, it needs testing of the whole client. CORS: **accepted** as is (the token is a header, not a cookie; limiting origins could break browser-based API users). Swagger at `/api-docs`: **accepted** public (it is the API documentation).
|
||||
**Case 4727, step 6 (2026-10-24):** every response carries `X-Content-Type-Options: nosniff`, `X-Frame-Options: SAMEORIGIN` and `Referrer-Policy: strict-origin` (the hosted nginx values, so the duplicates behind nginx agree); test `ApiRoute.SafeHeadersOnEveryResponse`. Still open from F35: the client Content-Security-Policy (later case); download keys in URLs stay (accepted with F7's fix).
|
||||
|
||||
### F36 Listening on all interfaces, no firewall (pass 10, from code)
|
||||
|
||||
Every `config.json` template (`lan-install-config.json`, `linux-server`, `linux-desktop`, the
|
||||
standalone launcher's) sets `AYANOVA_USE_URLS` to `http://*:7575`. On hosted droplets nginx
|
||||
proxies HTTPS to `127.0.0.1:7575`, but Kestrel also listens on the public address, and the setup
|
||||
script configures no host firewall. Unless a DigitalOcean cloud firewall blocks it (**John, 2026-10-23: every droplet has a restrictive
|
||||
cloud firewall**, the same settings confirmed for the Sockeye migration, so hosted droplets are
|
||||
not exposed; re-rated Low), anyone can reach AyaNova directly on port 7575 over plain HTTP, without TLS and
|
||||
without nginx's headers, and logins sent there travel in clear. The standalone (single-PC)
|
||||
install also accepts connections from the whole network. Fix: hosted and Linux server
|
||||
templates bind `http://127.0.0.1:7575` when behind a proxy; the setup script enables `ufw`
|
||||
allowing only 22, 80 and 443; standalone binds to localhost; LAN stays on all interfaces (that
|
||||
is its purpose) and the manual recommends HTTPS through IIS for anything beyond the LAN.
|
||||
**John, 2026-10-24:** standalone **left** on all interfaces (some users open it from another device; binding it to localhost would break them). Hosted: bind `127.0.0.1` in the `sockeye-client` setup script (to do there). `ufw`: not needed, every droplet has the restrictive cloud firewall.
|
||||
**Fixed for hosted in case 4727 (2026-10-26):** the setup script changes `AYANOVA_USE_URLS` to `http://127.0.0.1:7575` (`sockeye-client` `9f300a6`); existing droplets get the same change from the one-time `harden-830.sh` at the 8.3.0 rollout (see F38). Tested 2026-10-26 on `devtestn10` (Ubuntu 24.04.5), both paths, with the subscription zip of 2026-10-23: existing droplet with `harden-830.sh`, then a fresh install with the updated script (droplet rebuilt). Each time: AyaNova only on `127.0.0.1:7575`, the public address refused, HTTPS 200; `www-data` cannot write the program folder, can write `data`; a report render (including the first-use browser download into `data/report-browser`), a manual backup (pg_dump) and an attachment upload, download and delete all completed with no read-only or denied-access errors in the journal or the AyaNova log. `systemd-analyze security`: 9.2 to 8.3.
|
||||
|
||||
### F37 Standalone PostgreSQL trusts local connections (pass 10, from code)
|
||||
|
||||
`dist/win-x64/postgres-empty-database/pg_hba.conf` uses `trust` for local, 127.0.0.1 and ::1, and
|
||||
`listen_addresses` is the default (localhost). Any user or program on that PC can connect as any
|
||||
database user, the `postgres` superuser included, without a password. Standalone is a
|
||||
single-PC (mostly evaluation) install, so the exposure is local only; with F2 the database
|
||||
files are writable by every local user anyway. Fix: `scram-sha-256` with a password generated
|
||||
at install and written to the launcher's `config.json`; or accept it for evaluation installs
|
||||
(John's decision). **John, 2026-10-23: accepted** for standalone unless the fix is very short and
|
||||
low-risk; the scram change needs a generated password in the installer, the launcher config and a
|
||||
prepared database, so it is not that short: accepted.
|
||||
|
||||
### F38 Linux hardening (pass 10, from code)
|
||||
|
||||
- The systemd unit written by the setup script runs as `www-data` with no sandboxing options
|
||||
(`NoNewPrivileges`, `ProtectSystem=strict` with `ReadWritePaths=/var/ayanova/data`,
|
||||
`PrivateTmp`, `ProtectHome`).
|
||||
- `chmod g+rw /var/ayanova/` gives `www-data` (which nginx also runs as) write access to the
|
||||
program folder itself, so the service account can replace program files by renaming. Only
|
||||
`data` needs write access.
|
||||
- The Linux desktop install runs as root (`sudo ./AyaNova`, see Deferred), so the report browser,
|
||||
already started without Chrome's sandbox, runs as root too.
|
||||
- `dist/docker/linux-x64/docker-compose.yml` publishes PostgreSQL on port 5432 with a fixed
|
||||
superuser password. It is not built (`build-release.bat` lines are commented out) or
|
||||
documented; delete it or fix it before anyone uses it.
|
||||
|
||||
**John, 2026-10-24:** the `chmod` and the systemd sandboxing options go into the `sockeye-client` setup script (to do there; the systemd options need testing on a droplet so nothing that writes, such as logs, backups or the report browser download, is blocked). `dist/docker`: early development option, never offered: removed in case 4727, step 7 (2026-10-24; with its commented-out lines in `build-release.bat`; git history keeps it).
|
||||
**Fixed for hosted in case 4727 (2026-10-26):** the setup script (`sockeye-client` `9f300a6`) no longer gives the service group write access to the program folder (`chmod -R go-w /var/ayanova`, then group write on `data` only) and adds `NoNewPrivileges`, `ProtectSystem=strict` with `ReadWritePaths=/var/ayanova/data`, `PrivateTmp` and `ProtectHome` to the unit. Existing droplets get the same from a one-time `harden-830.sh`, run right before each droplet's 8.3.0 upgrade (in `devdocs/build/RELEASE-PROCEDURE.md`; the sandbox options as a drop-in, `ayanova.service.d/hardening.conf`; also `config.json` 640 and the old `Chrome` folders removed). Tested 2026-10-26 on `devtestn10` (Ubuntu 24.04.5), both paths, with the subscription zip of 2026-10-23: existing droplet with `harden-830.sh`, then a fresh install with the updated script (droplet rebuilt). Each time: AyaNova only on `127.0.0.1:7575`, the public address refused, HTTPS 200; `www-data` cannot write the program folder, can write `data`; a report render (including the first-use browser download into `data/report-browser`), a manual backup (pg_dump) and an attachment upload, download and delete all completed with no read-only or denied-access errors in the journal or the AyaNova log. `systemd-analyze security`: 9.2 to 8.3. Self-hosted Linux servers (case 4727, 2026-10-26): the manual's install steps now set the same permissions and service settings and, once NGINX works, bind `127.0.0.1` (`ops-install-linux-server.md`, `ops-config-use-urls.md`); the 8.3.0 upgrade step no longer re-adds group write on the program folder and offers the settings as a drop-in for existing installs; its upgrade commands use `curl -fO`, so a failed download stops before the program files are deleted (`ops-upgrade-linux-server.md`). Further systemd restrictions (kernel, address families, system calls) would each need the same test with the report browser and pg_dump: a later case if wanted. The desktop install running as root stays in Deferred.
|
||||
|
||||
OK in pass 10: the setup script generates a unique database password and JWT secret per
|
||||
droplet, stores them in `/root/ayanova-secrets.txt` (600), makes `config.json` 640 `root:www-data`,
|
||||
runs AyaNova as `www-data`, installs unattended upgrades, uses PGDG PostgreSQL on its default
|
||||
localhost listener, and gets a Let's Encrypt certificate with HSTS and the headers in F35.
|
||||
|
||||
### F41 The 2FA flaw (F5) in Sockeye and the business server (pass 11, from code)
|
||||
|
||||
Sockeye is a near-copy of this codebase: `sockeye/server/Controllers/AuthController.cs:168` matches
|
||||
the 2FA step on `TempToken` alone and sets it to `string.Empty` after success, exactly as F5.
|
||||
Sockeye's login is on the internet (sockeye.ayanova.com) and holds the customer list, licences
|
||||
and subscription servers; the business AyaNova server (8.2.x) has the same code. If an account on
|
||||
either has 2FA turned on, it can be reached with no password and a guessed code. **Immediate
|
||||
mitigation (no code change):** turn 2FA off on those accounts until the fix is deployed; with 2FA
|
||||
off the account is not matched by this query and the password protects it as usual. Then apply
|
||||
the F5 fix to Sockeye too. **John, 2026-10-23: 2FA turned off** on the affected accounts; the fix goes
|
||||
in the same case as F5 (one commit per repo). Other shared-code findings apply to Sockeye as well (F7, F8, F12, F19,
|
||||
F21, F30; F1 if its config kept a placeholder); its admin UI is used by John alone, so only the
|
||||
anonymous ones (F5, F41) are urgent there.
|
||||
|
||||
### F42 Licence-signing private keys in Sockeye source (pass 11, from code)
|
||||
|
||||
`sockeye/server/biz/RavenKeyFactory.cs` holds two RSA private keys as string literals (lines 158
|
||||
and 222; not reproduced here) and signs licence keys with them. They are therefore in the Sockeye
|
||||
repository and its history (self-hosted Gitea), in every build of Sockeye, and on the Sockeye
|
||||
droplet in the deployed DLL. Anyone who obtains any of those can create licence keys that every
|
||||
AyaNova server accepts. AyaNova checks licences against the matching public key
|
||||
(`License.cs`, "our real 2016 public key"). Fix: load the private key at run time from a file or
|
||||
environment variable on the Sockeye server only (root-readable, outside the repo and the
|
||||
published files) and remove it from the source. Rotation (new key pair, new public key in
|
||||
AyaNova, every licence reissued) is a business decision for John; without rotation the keys stay
|
||||
in history, so access to the repository and its backups must be treated as access to the keys.
|
||||
**John, 2026-10-23:** no rotation now; moving the key out of source becomes its own case for later
|
||||
(not an 8.3.0 blocker).
|
||||
|
||||
### F39 One-shot licence fetch by DB ID (pass 11, from code)
|
||||
|
||||
`RvfController` (`POST rvf`, anonymous) returns the newest active licence for a DB ID, once:
|
||||
`FetchedOn` is set on the first fetch and later fetches get 404. DB IDs are random (32 bytes) but
|
||||
not secret: the anonymous API root page shows it while a server is unlicensed (F11) and
|
||||
`server-info` shows it to every login (F29). Someone who knows it can take a newly issued key
|
||||
first; the real server then never receives it (a support case; the key only works for that DB
|
||||
ID, but it names the licensee and features). Fix: let the same DB ID fetch its key again (the key
|
||||
is bound to the DB ID, so one-shot adds nothing), and stop showing the DB ID anonymously.
|
||||
**AyaNova half fixed in case 4727, steps 2 and 3 (2026-10-24):** the DB ID is no longer shown anonymously (API root page) nor to customer contacts and subcontractors (`server-info`, client settings). The Sockeye half (repeatable fetch) is the later two-sided case with the server fingerprint.
|
||||
|
||||
### F40 Trial request routes (pass 11, from code)
|
||||
|
||||
`POST rvr` (AyaNova trial request) and `POST subscription-server-trial-request` are anonymous with no
|
||||
rate limit. Each `rvr` request with a new DB ID stores a request and sends Sockeye's fixed
|
||||
verification email to the address given; the email confirm code (`StringUtil.GenFetchCode`) is
|
||||
10 letters from `System.Random`, not a cryptographic generator. The subscription-server request's
|
||||
only bot check is a fixed value (`RValue == "totallylegit"`). Approval is manual, so none of this
|
||||
issues a licence; the exposure is email to arbitrary addresses and a cluttered request list.
|
||||
Fix: nginx `limit_req` on these routes; `RandomNumberGenerator` for codes; optionally a CAPTCHA
|
||||
on the website forms.
|
||||
|
||||
OK in pass 11: no licence is issued without John's approval; a DB ID with a purchased licence
|
||||
cannot request a trial; licence keys are bound to the DB ID and signed (AyaNova verifies the
|
||||
signature).
|
||||
|
||||
### F43 Unit pick-list variant in SQL (found 2026-10-24 during case 4712, from code)
|
||||
|
||||
`UnitPickList.GetVariantCriteria` takes a `customerid:<value>` variant from the request and returns
|
||||
`aunit.customerid = {v[1]}`, the value unparsed, which `PickListSqlBuilder` adds to the query. The
|
||||
route (`pick-list/list`, `pick-list/single`) is open to every login, customer contacts included. Same
|
||||
class as F18 and F26. **Correction to pass 6:** "Raw SQL: what was checked and found safe" said the
|
||||
pick-list variants were parsed as numbers or matched to fixed strings; that holds for the other six
|
||||
lists with variants but not this one (the pass 6 search looked for `{variant}` and `{crit[...]}`
|
||||
interpolations and missed `{v[1]}`). Fix: parse the value as a `long` and ignore the variant otherwise,
|
||||
as the other lists do.
|
||||
|
||||
### F2 Windows folder permissions (from the 8.3.0 installer test)
|
||||
|
||||
`lan.iss` and `standalone.iss` set `Permissions: users-modify` on `{app}` (the program
|
||||
folder) and on `{commonappdata}\ayanova` (data: database for standalone, backups,
|
||||
attachments, logs). Any local Windows user can therefore replace program files that run as
|
||||
the user who starts AyaNova or as the IIS application pool, and can read the database files
|
||||
and backups. Matters on shared or terminal-server machines. The program-folder permission
|
||||
originally let the report browser download beside the app; since 8.3.0 it downloads under
|
||||
the data folder, so `{app}` may no longer need it. To confirm in pass 10. **Pass 10:** confirmed from `lan.iss` and `standalone.iss`. `config.json`, with the database password and the JWT secret, is installed in `{app}`, so any local user can read those secrets and change the configuration or the program files; standalone also grants `users-modify` on `{app}\local-postgres` (the PostgreSQL binaries) and the database folder. Fix: drop `users-modify` from `{app}` and `local-postgres`; keep write access only where the running account needs it (logs, backups, temp, attachments, report browser), granted to that account or to Administrators rather than all Users; give `config.json` an ACL for Administrators and the service account. **Fixed in case 4579 (2026-10-25):** both installers no longer grant `users-modify` on `{app}` or `{app}\local-postgres`, and after copying files run `icacls "{app}" /remove:g *S-1-5-32-545 /T /C /Q` (Users group by SID), which also removes the old grant on upgrades; inherited read rights from Program Files stay. Nothing writes to `{app}` since 8.3.0. Proved in Windows Sandbox: single-user fresh install and upgrade over 8.2.4, LAN upgrade over the downloaded LAN installer: 4632 (single) and 1281 (LAN) Users modify entries before, 0 after; login, trial, attachment, backup and report render work after the upgrade. **Accepted (John, 2026-10-25), documented in the single-user and LAN install pages:** the data folder keeps `users-modify` and `config.json` (database password) stays readable by local users, because AyaNova runs as whichever user starts it (or the IIS app pool) and that account is not known at install time.
|
||||
|
||||
### F3 `notify/direct-smtp` open to every logged-in user
|
||||
|
||||
`NotifyController.SendNotifySmtpDirectMessage`: no role or user-type check; the code comment
|
||||
says this is intentional (Customer form, and API users emailing rendered reports, case 4310).
|
||||
Any logged-in user, customer contacts included, can send an email with any recipient
|
||||
(`ToAddress`), subject and HTML body through the company's configured SMTP server, from its
|
||||
address. On a hosted or internet-facing server one customer-contact login is enough to send
|
||||
phishing or spam under the company's name. To confirm in pass 8: which callers the client
|
||||
actually has, and the narrowest check that keeps them working (staff roles only; or
|
||||
recipient restricted to the object's own address). **Pass 8 (code):** confirmed. When `ToAddress` is given it is used as is (only when it is empty is the address taken from the Customer, HeadOffice, Vendor or User record); subject, text and HTML body come from the request; one recipient per call (`MailboxAddress.Parse`), no rate limit. The event log records each send. Fix: drop `ToAddress` from the route and always take the address from the record (the Customer form's use), check read rights on that record, and refuse customer-type users; API users who email rendered reports keep working through a record address.
|
||||
|
||||
### F4 `notify/direct-message` open to every logged-in user
|
||||
|
||||
Any logged-in user can add a general notification, with the sender's name and any text, for
|
||||
any user id. Low on its own (in-app only); relevant with F3 for social engineering of staff
|
||||
by a customer contact. Pass 8: confirmed from code (`NotifyController.SendNotifyDirectMessage`, no role check, any `ToUserId`). Fix: staff only, or only to users the caller may see.
|
||||
|
||||
**Fixed (F3, F4), 2026-10-24.** Customer contacts were already refused by the case 4712 route allowlist; case 4723 also refuses them in both actions. `direct-smtp`: an address taken from a Customer, HeadOffice, Vendor or User record needs read rights on that type (a contact's user record: User or Customer rights, as on the Contact form); the record checks run before the check that email delivery is on. Staff may still give any `ToAddress`, which John kept on purpose: the stock report template "z_API send SMTP from work order report" (`objectId 0`, `toAddress` from the work order) and at least one customer's report script rely on it. Every send is in the event log. Accepted remaining risk: a staff login can send email with any content to any address through the company's mail server. `direct-message`: refused to subcontractors too, as the client already hides it from them. Test `DirectSend` (refusals only, so no email is sent).
|
||||
|
||||
## Cases
|
||||
|
||||
John's cases for the findings (created 2026-10-24; full list in `NET10-PORT.md`, "Release 8.3.0
|
||||
cases"): 4710 (F5, F41), 4711 (F1), 4712 (F13 to F17), 4715 (F21), 4716 (F18), 4717 (F26),
|
||||
4718 (F19, F24), 4719 (F20), 4720 (F27), 4721 (F33), 4722 (F34), 4723 (F3, F4), 4724 (F30, F31),
|
||||
4725 (F7, F8, F9), 4726 (F6), 4579 (F2), 4713 (F28), 4727 (F11, F12, F22, F23, F25, F29, F35, F36, F38, and
|
||||
the AyaNova half of F39), Sockeye 4681 (F40). The Sockeye half of F39 (repeatable licence fetch) moved to a later case: AyaNova reinstalls and logs any key it fetches without comparing it with the installed one (trial servers with data would log an E1020 error on every fetch), and deployed older servers cannot be changed, so it needs an AyaNova-side change plus a request flag that Sockeye honours. Not in 8.3.0: F10 (case 4736), F32, F42 (John's later cases); F37 accepted. When a
|
||||
case is done, its findings' status becomes *fixed in case NNNN*.
|
||||
|
||||
## Customer contact route allowlist (implemented in case 4712)
|
||||
|
||||
The API routes the client can call for a customer contact (`UserType` Customer or HeadOffice),
|
||||
read from `raven-client` on 2026-10-23: the customer menu in `api/initialize.js` (user
|
||||
settings, notification subscriptions, work orders, service requests), the screens behind it
|
||||
(`customer-workorders`, `customer-workorder`, `customer-csr`, `customer-csr-edit`,
|
||||
`home-user-settings`, `home-password`, `home-security`, `home-notify-subscription(s)`,
|
||||
`home-notifications`, `ay-about`, `ay-report-view`, `login`, `home-reset`), and the components
|
||||
they use. Staff-only parts of shared components are left out: data-table extensions (export,
|
||||
batch delete, tags, direct SMTP) are not on customer screens, attachments and wiki are
|
||||
read-only there, and CSR accept/reject are staff menu items. Method used:
|
||||
`customer_routes.py` (kept outside the repo) follows each screen's imports and `gz-` components
|
||||
and lists every `$gz.api` call; the result was then checked by hand.
|
||||
|
||||
Everything not on this list would answer 403 to a customer-type user. **Ownership** says what
|
||||
each id route must also check.
|
||||
|
||||
| Area | Routes | Ownership check needed |
|
||||
|---|---|---|
|
||||
| Login, session (already anonymous or exempt) | `auth`, `auth/tfa-authenticate`, `auth/reset-password`, `notify/hello`, `translation/subset[/{id}]`, `logo/{size}`, `license/lc` (F10) | — |
|
||||
| Own account | `auth/change-password`, `auth/totp`, `auth/totp-validate`, `auth/totp-disable/{id}`, `user-option/{id}` GET and PUT | `{id}` = the caller (`totp-disable` already; check `user-option`) |
|
||||
| App shell | `global-biz-setting/client`, `enum-list/list/{key}`, `form-custom/{formkey}`, `translation/list`, `translation/subset` (logged-in version, used to load translations), `server-info`, `notify/new-count`, `notify/app-notifications`, `DELETE notify/{id}` | `notify/{id}`: the caller's own notification (pass 7) |
|
||||
| Data lists (work orders, CSRs) | `data-list` (server already forces the customer filter), `data-list/listfields`, `data-list-column-view…`, `data-list-filter…`, `tag-list/list`, `pick-list/list` from the filter editor | saved filters: already scoped to the user |
|
||||
| Work order (customer view) | `workorder/customer/{id}`, `attachment/list` (work order only), `attachment/download/{id}` (download key), `report/render-job` with report `-100` only, `job-operations/status|logs/{gid}`, `report/request-cancel/{gid}`, `report/download/{file}` | work order belongs to the contact's customer (or a customer of its head office): **F13**; job and report file belong to the caller (pass 4) |
|
||||
| Service requests | `csr` POST/PUT, `csr/{id}` GET/DELETE (not `accept`/`reject`), `unit/customerunit` POST, `pick-list/list` and `pick-list/single` for Customer (head-office variant), Unit (own customer) and UnitModel only, with the server's own template | CSR and unit belong to the contact's customer; `CustomerId` forced on create: **F14**, **F15** |
|
||||
| Notification subscriptions | `notify-subscription/list`, `notify-subscription/{id}` GET/DELETE, `notify-subscription` POST/PUT, `work-order-status/list`, `quote-status/list` (names only, used by the editor) | `UserId` forced to the caller: **F17** |
|
||||
|
||||
Confirmed while fixing (case 4712): for customer users the client's open-object handler only navigates
|
||||
to the customer screens, so its staff routes (`attachment/parent`, `form-custom/form-key`,
|
||||
`user/inside-type`, `search/ancestor`) are not needed; `server-info` stays open (the About page calls it
|
||||
for everyone; F29 trims what it returns). The list lives in
|
||||
`server/AyaNova/ControllerHelpers/CustomerContactRoutes.cs`; the token middleware answers 403 to a
|
||||
contact for anything else (also closes F8's suffix exemption for contacts).
|
||||
|
||||
**Shape of the fix.** One check in the token middleware, which already loads `UserType` for
|
||||
every request: for Customer and HeadOffice users, compare the matched endpoint's route
|
||||
template (`context.GetEndpoint()`, available because `UseRouting` runs first) with this
|
||||
allowlist and answer 403 otherwise. New routes then stay closed to contacts unless added
|
||||
on purpose. The ownership checks stay in the routes themselves (F13, F14, F15, F17). F16
|
||||
(schedule) is closed by the allowlist alone; its staff role check is still needed for staff
|
||||
roles that should not see the schedule (pass 7).
|
||||
|
||||
## Pass log
|
||||
|
||||
| Date | Pass | Result |
|
||||
|---|---|---|
|
||||
| 2026-10-23 | 1 Threat model and map | Scope agreed with John. Middleware, JWT validation, role model, download keys, CORS and Swagger read. Route map generated (460 actions; script kept in `security/routemap.py`). No route lacks authentication by accident. 53 logged-in routes have no role check in the action: 3 resolved (erase data, trial seed: OK; direct SMTP: F3), the rest assigned to later passes. Local-request exception not reachable through the hosted nginx. Findings F1 to F4 recorded as candidates |
|
||||
| 2026-10-23 | 2 Login, tokens, reset, 2FA, anonymous routes | Read `AuthController`, the token middleware, `Hasher`, download-key check, password reset, TOTP setup, and every anonymous route (`notify/hello`, `license/lc`, `translation/subset`, `logo`, API root, health). Tested on the local dev server with the seeded `Inventory` user: **F5 critical, confirmed** (2FA step without password); **F1 confirmed** (current token rebuilt from the secret in 6 tries); F8 confirmed. New: F6, F7, F9 to F12. OK: reset codes (32 random characters, 48 h, cleared on use, same reply for unknown codes), login replies do not reveal whether a login exists, roles read from the database on every request, deactivation ends the session, TOTP secret 160 bits. Leads for pass 4: `AuthController.GenRpt` issues SuperUser (id 1) tokens with an `rpl` claim for report rendering; pass 5: the logo is served with the content type given at upload |
|
||||
| 2026-10-23 | 3 Customer contacts | Mapped the `Customer` role in `BizRoles.cs` and every customer-reachable route. Tested on the local dev server as seeded contact `Customer` (customer 1), read-only: F13, F14 (read), F15, F16 confirmed. F17 and F18 from code (F17 not tested because it sends email; F18 not tested by choice). OK: data lists (server sets the customer filter), KPIs (no customer roles), saved filters and notification subscription reads (scoped to the user), memos (no customer access) |
|
||||
| 2026-10-23 | 4 Report rendering | Read `ReportController`, `ReportBiz` (render path, data, browser launch), `ay-report.js` helpers, `CoreJobCustomerNotify` and `AuthController.GenRpt`. Tested on the local dev server: **F19 confirmed** (contact imported a stock template copy, deleted afterwards). F20, F21 and F22 from code; rendering of a crafted template and the download path were deliberately not tested. OK: `report/data` and `render-job` check read rights on the record type; template API calls use the renderer's own token (except notification reports, F20); report output files have random names. Pass 5 picks up F21's other routes |
|
||||
| 2026-10-23 | 5 File handling | Read `ApiUploadProcessor`, `AttachmentController` (upload, list, download, move, delete, maintenance), `LogoController`, `ExportController`, `TranslationController` upload, `LogFilesController`, `BackupController`, `ImportController`, and the path helpers in `FileUtil`. No live tests (the role gaps have the same shape as F19, already confirmed). New: F23, F24, F25; F21 extended to the log and backup download routes (ops roles), backup delete found safe |
|
||||
| 2026-10-23 | 6 Raw SQL | Code reading only. Listed every hand-built query (`CommandText`, `*SqlRaw`, interpolated SQL) and traced each request value into it: data-list filter, sort and select builders, all 51 `ClientCriteria` users, saved filters, search tokenizer, every KPI's criteria, pick-list builder and variants, customer notification query, controllers and biz classes with raw SQL. New: **F26** (KPI `interval`). F18 completed by code. Everything else safe (see "Raw SQL: what was checked") |
|
||||
| 2026-10-23 | 7 Staff roles | Went through the remaining logged-in routes without a role check, the restricted-role filtering in `WorkOrderBiz`, and user and contact administration in `UserBiz`. Tested on the local dev server, read-only: **F27 confirmed** (`SubContractorRestricted` through `report/data`). F28 from code (live test not done). New: F29. The rest of the list found OK (see "Pass 7: routes ... found OK") |
|
||||
| 2026-10-23 | 8 Email and notifications | Code reading only, nothing sent. Read `NotifyController` (direct SMTP and message), `Mailer`, `GlobalOpsNotificationSettings` controller, biz and model, `NotifyEventHelper`, `CoreJobNotify`, `CoreJobCustomerNotify`, `NotifySubscriptionBiz`. F3 and F4 confirmed by code; F17 confirmed and re-rated Medium (names and links, no reports). New: F30, F31, F32. **Action for John: rotate the FastMail app password in F32** |
|
||||
| 2026-10-23 | 9 Client, headers, CORS, Swagger | Code reading of every `v-html` use in `raven-client` and its data source; `npm audit` read-only against the lockfile (no install, no change), triaged to shipped packages; server and nginx headers, CORS, Swagger. New: F33, F34 (needs John's decision on two front-end packages), F35; F14 extended (`unit/customerunit`) |
|
||||
| 2026-10-23 | 10 Deployment | Code reading: `lan.iss`, `standalone.iss`, the four `config.json` templates, the standalone PostgreSQL configuration, the hosted setup script in `sockeye-client` (secrets, permissions, systemd unit, nginx, certbot), `dist/docker`. F2 confirmed and extended (`config.json` readable by all users); F9 extended (hosted droplets start with the default SuperUser password). New: F36, F37, F38. John then confirmed hosted droplets have a restrictive cloud firewall (F36 re-rated Low), accepted F37, and explained the trial default password (F9 note added) |
|
||||
| 2026-10-23 | 11 Sockeye public routes | Code reading of `RvfController` (licence fetch), `RvrController` and `SubscriptionServerTrialRequestController` (trial requests), `TrialLicenseRequestBiz`, the code generator, Sockeye's `AuthController` and token middleware, and where licences are signed. New: **F41** (F5 in Sockeye and the business server; turn 2FA off until fixed), **F42** (signing private keys in source), F39, F40. **All 11 passes done** |
|
||||
@@ -7,7 +7,7 @@ What test suites exist, what they protect, and the rules for adding to them.
|
||||
| Suite | Where | What it covers | State |
|
||||
|---|---|---|---|
|
||||
| API integration tests | `raven-test-integration` | Tier 1–3 API coverage against a running server | Green apart from three known server bugs (March 2026). Details in that repo's `CLAUDE.md` |
|
||||
| E2E | Cypress 9.7.0 **[unverified: which folder]** | User workflows in a browser, against remote devtest servers, with version validation and environment-based config | **[unverified]** |
|
||||
| E2E | `raven-test\e2e` (separate repo `raven-test`), Cypress 9.7.0 | User workflows in a browser, against remote devtest servers, with version validation and environment-based config | **[unverified]**: last commit 2026-02-22; current pass state unknown |
|
||||
| Licence verification gate | To be written (`NET10-PORT.md`, section 0) | Every stored v8 licence still verifies | Not started |
|
||||
| Unit tests | none | — | Roadmap step E |
|
||||
|
||||
|
||||
51
devdocs/modernization/security/routemap.py
Normal file
51
devdocs/modernization/security/routemap.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# Map every controller action: HTTP verb + route, anonymous or not, and which checks its body makes.
|
||||
# Usage: python -I routemap.py <Controllers folder> <out.tsv>
|
||||
import os, re, sys
|
||||
|
||||
src, out = sys.argv[1], sys.argv[2]
|
||||
verb_re = re.compile(r'\[Http(Get|Post|Put|Delete|Patch)(?:\("([^"]*)"\))?\]')
|
||||
method_re = re.compile(r'public\s+(?:async\s+)?[\w<>\[\], ]+\s+(\w+)\s*\(')
|
||||
checks = {
|
||||
'role': re.compile(r'Authorized\.Has\w+Role|HasAnyRole|HasReadFullRole|HasModifyRole|HasCreateRole|HasDeleteRole|HasSelectRole'),
|
||||
'usertype': re.compile(r'UserTypeFromContext|UserType\.Customer|UserType\.HeadOffice|IsCustomerUser|CustomerUser'),
|
||||
'biz': re.compile(r'Biz\.GetBiz|new \w+Biz\(|\w+Biz\.'),
|
||||
'dltoken': re.compile(r'ValidateDownloadToken'),
|
||||
}
|
||||
|
||||
rows = []
|
||||
for fn in sorted(os.listdir(src)):
|
||||
if not fn.endswith('.cs'):
|
||||
continue
|
||||
text = open(os.path.join(src, fn), encoding='utf-8-sig').read()
|
||||
# drop // comments so commented-out actions are ignored
|
||||
text = '\n'.join(l for l in text.splitlines() if not l.lstrip().startswith('//'))
|
||||
m = re.search(r'\[Route\("([^"]*)"\)\]', text)
|
||||
base = m.group(1).replace('api/v{version:apiVersion}/', '') if m else ''
|
||||
cls_auth = bool(re.search(r'\[Authorize\][^{]*class\s+\w+Controller', text, re.S))
|
||||
for vm in verb_re.finditer(text):
|
||||
after = text[vm.end():]
|
||||
mm = method_re.search(after)
|
||||
if not mm:
|
||||
continue
|
||||
attrs = text[max(0, vm.start() - 300):vm.end()] + after[:mm.start()]
|
||||
anon = '[AllowAnonymous]' in attrs
|
||||
# body: from the method's opening brace to the matching closing brace
|
||||
start = vm.end() + mm.end()
|
||||
brace = text.find('{', start)
|
||||
depth, i = 0, brace
|
||||
while i < len(text):
|
||||
if text[i] == '{': depth += 1
|
||||
elif text[i] == '}':
|
||||
depth -= 1
|
||||
if depth == 0: break
|
||||
i += 1
|
||||
body = text[brace:i]
|
||||
found = [k for k, r in checks.items() if r.search(body)]
|
||||
rows.append((fn.replace('Controller.cs', ''), vm.group(1).upper(), (base + '/' + (vm.group(2) or '')).rstrip('/'),
|
||||
mm.group(1), 'ANON' if anon else ('auth' if cls_auth or '[Authorize]' in attrs else 'NO-ATTR'), ','.join(found) or '-'))
|
||||
|
||||
with open(out, 'w', encoding='utf-8') as f:
|
||||
f.write('controller\tverb\troute\taction\taccess\tchecks\n')
|
||||
for r in rows:
|
||||
f.write('\t'.join(r) + '\n')
|
||||
print(len(rows), 'actions')
|
||||
7
dist/assets/LICENSE
vendored
7
dist/assets/LICENSE
vendored
@@ -51,7 +51,7 @@ Ground Zero will comply with all applicable laws pertaining to personal data inc
|
||||
|
||||
A: Data obtained
|
||||
|
||||
Information about you (such as your email address, name, company information) is provided to us when you email us (such as when you email us directly, email us via our online form, email us via our online temporary Key request form) or when you register on our AyaNova Support Forum.
|
||||
Information about you (such as your email address, name, company information) is provided to us when you email us (such as when you email us directly, email us via our online form, email us via our online temporary Key request form).
|
||||
|
||||
Information you provided in your order (such as your company name, company contact information, contact name, along with the AyaNova products/services/subscriptions purchased) is sent to us from our payment processing partner with whom you placed your order.
|
||||
|
||||
@@ -61,7 +61,7 @@ Ground Zero will not share your information for marketing purposes with companie
|
||||
|
||||
C: Use and Sharing of Data
|
||||
|
||||
Personal data you provided when you contact us (such as when you email us directly, email us via our online form, email us via our online temporary Key request form or register on our AyaNova Support Forum) will be used to provide technical support, to respond to user inquiries, to provide temporary Key(s), to maintain case and bug issues reported by you, and to send you relevant AyaNova information.
|
||||
Personal data you provided when you contact us (such as when you email us directly, email us via our online form, or email us via our online temporary Key request form) will be used to provide technical support, to respond to user inquiries, to provide temporary Key(s), to maintain case and bug issues reported by you, and to send you relevant AyaNova information.
|
||||
|
||||
Ground Zero will also receive your company information you provided to our payment processing partner, so that we may process your product/service/subscription order.
|
||||
|
||||
@@ -83,8 +83,7 @@ E: Deleting of Data
|
||||
(i) Temporary key requests without subsequent purchase will be deleted within one year.
|
||||
(ii) Email correspondence without subsequent purchase will be deleted within two years except where required for maintaining case and support history.
|
||||
(iii) Email correspondence including temporary key requests with subsequent subscription purchase will be retained for the life of your subscription. Once a subscription has been allowed to lapse (not renewed), correspondence will be deleted within two years except where required for maintaining case and support history.
|
||||
(iv) Customer information obtained through AyaNova Support Forum registration will be deleted within three years of forum login inactivity.
|
||||
(v) Purchase information along with company information as provided by our payment processing partner will be retained for a minimum of six years after the end of the taxation year to which they relate as per the Canada Revenue Agency and no longer than 10 years after which will be deleted within one year.
|
||||
(iv) Purchase information along with company information as provided by our payment processing partner will be retained for a minimum of six years after the end of the taxation year to which they relate as per the Canada Revenue Agency and no longer than 10 years after which will be deleted within one year.
|
||||
|
||||
|
||||
I HAVE READ AND UNDERSTOOD ALL OF THE ABOVE TERMS AND CONDITIONS AND ACKNOWLEDGE THAT THESE TERMS AND CONDITIONS FORM PART OF THIS AGREEMENT. I AGREE THAT A PRINT-OUT OF THIS AGREEMENT WILL CONSTITUTE A "WRITING" UNDER ANY APPLICABLE LAWS.
|
||||
|
||||
5
dist/assets/lan-install-config.json
vendored
5
dist/assets/lan-install-config.json
vendored
@@ -3,9 +3,8 @@
|
||||
"AYANOVA_DB_CONNECTION":"Server=localhost;Username=postgres;Password=YOUR_PASSWORD_HERE;Database=AyaNova;CommandTimeout=300;",
|
||||
"AYANOVA_DEFAULT_TRANSLATION":"en",
|
||||
"AYANOVA_DATA_PATH":"%ProgramData%\\ayanova",
|
||||
"AYANOVA_BACKUP_PG_DUMP_PATH": "C:\\Program Files\\PostgreSQL\\16\\bin",
|
||||
"AYANOVA_LOG_LEVEL": "Info",
|
||||
"AYANOVA_JWT_SECRET": "11111YourRandom32CharacterSecret"
|
||||
"AYANOVA_BACKUP_PG_DUMP_PATH": "C:\\Program Files\\PostgreSQL\\18\\bin",
|
||||
"AYANOVA_LOG_LEVEL": "Info"
|
||||
}
|
||||
|
||||
|
||||
|
||||
7
dist/assets/license.rtf
vendored
7
dist/assets/license.rtf
vendored
@@ -54,7 +54,7 @@ Ground Zero will comply with all applicable laws pertaining to personal data inc
|
||||
\f0\fs20\par
|
||||
A: Data obtained\f1\fs24\par
|
||||
\f0\fs20\par
|
||||
Information about you (such as your email address, name, company information) is provided to us when you email us (such as when you email us directly, email us via our online form, email us via our online temporary Key request form) or when you register on our AyaNova Support Forum.\f1\fs24\par
|
||||
Information about you (such as your email address, name, company information) is provided to us when you email us (such as when you email us directly, email us via our online form, email us via our online temporary Key request form).\f1\fs24\par
|
||||
\f0\fs20\par
|
||||
Information you provided in your order (such as your company name, company contact information, contact name, along with the AyaNova products/services/subscriptions purchased) is sent to us from our payment processing partner with whom you placed your order.\f1\fs24\par
|
||||
\f0\fs20\par
|
||||
@@ -64,7 +64,7 @@ Ground Zero will not share your information for marketing purposes with companie
|
||||
\f0\fs20\par
|
||||
C: Use and Sharing of Data\f1\fs24\par
|
||||
\f0\fs20\par
|
||||
Personal data you provided when you contact us (such as when you email us directly, email us via our online form, email us via our online temporary Key request form or register on our AyaNova Support Forum) will be used to provide technical support, to respond to user inquiries, to provide temporary Key(s), to maintain case and bug issues reported by you, and to send you relevant AyaNova information.\f1\fs24\par
|
||||
Personal data you provided when you contact us (such as when you email us directly, email us via our online form, or email us via our online temporary Key request form) will be used to provide technical support, to respond to user inquiries, to provide temporary Key(s), to maintain case and bug issues reported by you, and to send you relevant AyaNova information.\f1\fs24\par
|
||||
\f0\fs20\par
|
||||
Ground Zero will also receive your company information you provided to our payment processing partner, so that we may process your product/service/subscription order.\f1\fs24\par
|
||||
\f0\fs20\par
|
||||
@@ -86,8 +86,7 @@ E: Deleting of Data \f1\fs24\par
|
||||
(i) Temporary key requests without subsequent purchase will be deleted within one year.\f1\fs24\par
|
||||
\f0\fs20 (ii) Email correspondence without subsequent purchase will be deleted within two years except where required for maintaining case and support history.\f1\fs24\par
|
||||
\f0\fs20 (iii) Email correspondence including temporary key requests with subsequent subscription purchase will be retained for the life of your subscription. Once a subscription has been allowed to lapse (not renewed), correspondence will be deleted within two years except where required for maintaining case and support history. \f1\fs24\par
|
||||
\f0\fs20 (iv) Customer information obtained through AyaNova Support Forum registration will be deleted within three years of forum login inactivity. \f1\fs24\par
|
||||
\f0\fs20 (v) Purchase information along with company information as provided by our payment processing partner will be retained for a minimum of six years after the end of the taxation year to which they relate as per the Canada Revenue Agency and no longer than 10 years after which will be deleted within one year. \f1\fs24\par
|
||||
\f0\fs20 (iv) Purchase information along with company information as provided by our payment processing partner will be retained for a minimum of six years after the end of the taxation year to which they relate as per the Canada Revenue Agency and no longer than 10 years after which will be deleted within one year. \f1\fs24\par
|
||||
\f0\fs20\par
|
||||
\par
|
||||
I HAVE READ AND UNDERSTOOD ALL OF THE ABOVE TERMS AND CONDITIONS AND ACKNOWLEDGE THAT THESE TERMS AND CONDITIONS FORM PART OF THIS AGREEMENT. I AGREE THAT A PRINT-OUT OF THIS AGREEMENT WILL CONSTITUTE A "WRITING" UNDER ANY APPLICABLE LAWS.\f1\fs24\par
|
||||
|
||||
3
dist/assets/linux-server/config.json
vendored
3
dist/assets/linux-server/config.json
vendored
@@ -3,6 +3,5 @@
|
||||
"AYANOVA_DB_CONNECTION":"Server=localhost;Username=postgres;Password=YOUR_PASSWORD_HERE;Database=AyaNova;CommandTimeout=300;",
|
||||
"AYANOVA_DEFAULT_TRANSLATION":"en",
|
||||
"AYANOVA_DATA_PATH":"/var/ayanova/data/",
|
||||
"AYANOVA_LOG_LEVEL": "Info",
|
||||
"AYANOVA_JWT_SECRET": "12345YourRandom32CharacterSecret"
|
||||
"AYANOVA_LOG_LEVEL": "Info"
|
||||
}
|
||||
15
dist/docker/linux-x64/ayanovadocker/dockerfile
vendored
15
dist/docker/linux-x64/ayanovadocker/dockerfile
vendored
@@ -1,15 +0,0 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine
|
||||
WORKDIR /app
|
||||
COPY ./files .
|
||||
RUN apk add libgdiplus --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing/
|
||||
RUN apk add postgresql-client
|
||||
RUN apk add --no-cache \
|
||||
chromium \
|
||||
nss \
|
||||
freetype \
|
||||
freetype-dev \
|
||||
harfbuzz \
|
||||
ca-certificates \
|
||||
ttf-freefont
|
||||
|
||||
ENTRYPOINT ["dotnet", "AyaNova.dll"]
|
||||
44
dist/docker/linux-x64/docker-compose.yml
vendored
44
dist/docker/linux-x64/docker-compose.yml
vendored
@@ -1,44 +0,0 @@
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
|
||||
|
||||
postgresserver:
|
||||
image: postgres:alpine
|
||||
restart: always
|
||||
ports:
|
||||
- 5432:5432
|
||||
environment:
|
||||
POSTGRES_PASSWORD: letmein
|
||||
volumes:
|
||||
- /var/lib/ayanova/db:/var/lib/postgresql/data
|
||||
|
||||
ayanova:
|
||||
image: gztw/ayanova:v8.0.0
|
||||
init: true
|
||||
restart: always
|
||||
ports:
|
||||
- 7575:7575
|
||||
volumes:
|
||||
- /var/lib:/var/lib
|
||||
environment:
|
||||
AYANOVA_USE_URLS: http://*:7575
|
||||
AYANOVA_DB_CONNECTION: User ID=postgres;Password=letmein;Host=postgresserver;Port=5432;Database=AyaNova;Pooling=true;CommandTimeout=90;
|
||||
AYANOVA_ATTACHMENT_FILES_PATH: /var/lib/ayanova/files/user
|
||||
AYANOVA_BACKUP_FILES_PATH: /var/lib/ayanova/files/backup
|
||||
AYANOVA_TEMP_FILES_PATH: /var/lib/ayanova/files/temp
|
||||
AYANOVA_LOG_PATH: /var/lib/ayanova
|
||||
AYANOVA_LOG_LEVEL: Info
|
||||
AYANOVA_REPORT_RENDER_BROWSER_PATH: /usr/bin/chromium-browser
|
||||
build:
|
||||
context: ./ayanovadocker
|
||||
dockerfile: Dockerfile
|
||||
links:
|
||||
- postgresserver
|
||||
depends_on:
|
||||
- "postgresserver"
|
||||
|
||||
networks:
|
||||
default:
|
||||
external:
|
||||
name: docker-network
|
||||
23
dist/install/windows/x64/lan.iss
vendored
23
dist/install/windows/x64/lan.iss
vendored
@@ -1,7 +1,7 @@
|
||||
; LAN install for internal network use only
|
||||
|
||||
#define MyAppName "AyaNova server"
|
||||
#define MyAppVersion "8.2.4"
|
||||
#define MyAppVersion "8.3.1"
|
||||
#define MyAppPublisher "Ground Zero Tech-Works, Inc."
|
||||
#define MyAppURL "https://ayanova.com/"
|
||||
#define MyAppLauncherExeName "ayanova-launcher.exe"
|
||||
@@ -43,16 +43,19 @@ Source: "C:\data\code\raven\dist\assets\open-ayanova-app.bat"; DestDir: "{app}";
|
||||
|
||||
|
||||
[Dirs]
|
||||
Name: "{app}";Permissions: users-modify;
|
||||
Name: "{app}\Chrome";Permissions: users-modify;Flags: uninsalwaysuninstall
|
||||
Name: "{app}\ChromeHeadlessShell";Permissions: users-modify;Flags: uninsalwaysuninstall
|
||||
Name: "{commonappdata}\ayanova";Permissions: users-modify;
|
||||
Name: "{commonappdata}\ayanova\logs";Permissions: users-modify;
|
||||
|
||||
|
||||
[UninstallDelete]
|
||||
Type: filesandordirs; Name: "{app}\Chrome\*"
|
||||
Type: filesandordirs; Name: "{app}\ChromeHeadlessShell\*"
|
||||
Type: filesandordirs; Name: "{app}\Chrome"
|
||||
Type: filesandordirs; Name: "{app}\ChromeHeadlessShell"
|
||||
|
||||
[Run]
|
||||
; case 4579: the program folder is no longer writable by all local users (nothing writes there, data is under
|
||||
; {commonappdata}\ayanova). Upgrades over 8.2.4 or older still carry the old grant, so remove the Users group's own
|
||||
; rights (by SID, the group name is translated) from the program folder and everything in it; inherited read rights stay
|
||||
Filename: "{sys}\icacls.exe"; Parameters: """{app}"" /remove:g *S-1-5-32-545 /T /C /Q"; Flags: runhidden waituntilterminated
|
||||
|
||||
[Icons]
|
||||
Name: "{autoprograms}\AyaNova server\{#MyAppName}"; Filename: "{app}\AyaNova.exe";IconFilename: "{app}\AyaNova.exe";
|
||||
@@ -69,13 +72,13 @@ var
|
||||
begin
|
||||
if (CurStep=ssInstall) then
|
||||
begin
|
||||
if MsgBox('AyaNova requires the "ASP.NET Core Runtime Hosting bundle" 8.x.' + #13#10 +'Open the download web page now?', mbConfirmation, MB_YESNO or MB_DEFBUTTON2) = IDYES then
|
||||
if MsgBox('AyaNova requires the "ASP.NET Core Runtime Hosting bundle" 10.x.' + #13#10 +'Open the download web page now?', mbConfirmation, MB_YESNO or MB_DEFBUTTON2) = IDYES then
|
||||
begin
|
||||
ShellExec('open', 'https://dotnet.microsoft.com/en-us/download/dotnet/8.0', '', '', SW_SHOW, ewNoWait, ErrCode);
|
||||
ShellExec('open', 'https://dotnet.microsoft.com/en-us/download/dotnet/10.0', '', '', SW_SHOW, ewNoWait, ErrCode);
|
||||
end;
|
||||
if MsgBox('AyaNova requires a PostgreSQL db server.' + #13#10 +'Open the PostgreSQL installation instructions web page now?', mbConfirmation, MB_YESNO or MB_DEFBUTTON2) = IDYES then
|
||||
if MsgBox('AyaNova requires a PostgreSQL db server.' + #13#10 +'Open the PostgreSQL download page now?', mbConfirmation, MB_YESNO or MB_DEFBUTTON2) = IDYES then
|
||||
begin
|
||||
ShellExec('open', 'https://www.postgresqltutorial.com/install-postgresql/', '', '', SW_SHOW, ewNoWait, ErrCode);
|
||||
ShellExec('open', 'https://www.postgresql.org/download/windows/', '', '', SW_SHOW, ewNoWait, ErrCode);
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
50
dist/install/windows/x64/standalone.iss
vendored
50
dist/install/windows/x64/standalone.iss
vendored
@@ -3,7 +3,7 @@
|
||||
; external to lan requires different config
|
||||
|
||||
#define MyAppName "AyaNova"
|
||||
#define MyAppVersion "8.2.4"
|
||||
#define MyAppVersion "8.3.1"
|
||||
#define MyAppPublisher "Ground Zero Tech-Works, Inc."
|
||||
#define MyAppURL "https://ayanova.com/"
|
||||
#define MyAppLauncherExeName "ayanova-launcher.exe"
|
||||
@@ -45,22 +45,34 @@ Source: "C:\data\code\raven\dist\win-x64\ayanova\standalone\AyaNova.exe"; DestDi
|
||||
Source: "C:\data\code\raven\dist\win-x64\ayanova\standalone\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "C:\data\code\raven\dist\win-x64\launcher\config.json"; DestDir: "{app}"; Flags: ignoreversion confirmoverwrite
|
||||
Source: "C:\data\code\raven\dist\win-x64\launcher\*"; DestDir: "{app}\launcher"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "C:\data\code\raven\dist\win-x64\postgres-standalone\*"; DestDir: "{app}\local-postgres"; Permissions: users-modify;Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "C:\data\code\raven\dist\win-x64\postgres-standalone\*"; DestDir: "{app}\local-postgres"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "C:\data\code\raven\dist\win-x64\postgres-empty-database\*"; DestDir: "{commonappdata}\ayanova\database"; Permissions: users-modify; Flags: ignoreversion recursesubdirs createallsubdirs onlyifdoesntexist uninsneveruninstall
|
||||
|
||||
[Dirs]
|
||||
;Name: "{app}\.local-chromium";Permissions: users-modify;Flags: uninsalwaysuninstall
|
||||
Name: "{app}";Permissions: users-modify;
|
||||
Name: "{app}\Chrome";Permissions: users-modify;Flags: uninsalwaysuninstall
|
||||
Name: "{app}\ChromeHeadlessShell";Permissions: users-modify;Flags: uninsalwaysuninstall
|
||||
Name: "{commonappdata}\ayanova";Permissions: users-modify;Flags: uninsneveruninstall
|
||||
Name: "{commonappdata}\ayanova\logs";Permissions: users-modify;Flags: uninsneveruninstall
|
||||
; Empty folders PostgreSQL 14 needs in a new database. Git does not keep empty folders, so dist\win-x64\postgres-empty-database
|
||||
; (checked out from Git) lacks them and the server will not start on a fresh install. Already present on an upgrade.
|
||||
Name: "{commonappdata}\ayanova\database\pg_commit_ts";Permissions: users-modify;Flags: uninsneveruninstall
|
||||
Name: "{commonappdata}\ayanova\database\pg_dynshmem";Permissions: users-modify;Flags: uninsneveruninstall
|
||||
Name: "{commonappdata}\ayanova\database\pg_logical\mappings";Permissions: users-modify;Flags: uninsneveruninstall
|
||||
Name: "{commonappdata}\ayanova\database\pg_logical\snapshots";Permissions: users-modify;Flags: uninsneveruninstall
|
||||
Name: "{commonappdata}\ayanova\database\pg_notify";Permissions: users-modify;Flags: uninsneveruninstall
|
||||
Name: "{commonappdata}\ayanova\database\pg_replslot";Permissions: users-modify;Flags: uninsneveruninstall
|
||||
Name: "{commonappdata}\ayanova\database\pg_serial";Permissions: users-modify;Flags: uninsneveruninstall
|
||||
Name: "{commonappdata}\ayanova\database\pg_snapshots";Permissions: users-modify;Flags: uninsneveruninstall
|
||||
Name: "{commonappdata}\ayanova\database\pg_stat";Permissions: users-modify;Flags: uninsneveruninstall
|
||||
Name: "{commonappdata}\ayanova\database\pg_stat_tmp";Permissions: users-modify;Flags: uninsneveruninstall
|
||||
Name: "{commonappdata}\ayanova\database\pg_tblspc";Permissions: users-modify;Flags: uninsneveruninstall
|
||||
Name: "{commonappdata}\ayanova\database\pg_twophase";Permissions: users-modify;Flags: uninsneveruninstall
|
||||
Name: "{commonappdata}\ayanova\database\pg_wal\archive_status";Permissions: users-modify;Flags: uninsneveruninstall
|
||||
|
||||
|
||||
[UninstallDelete]
|
||||
;Type: filesandordirs; Name: "{app}\.local-chromium\*"
|
||||
Type: filesandordirs; Name: "{app}\Chrome\*"
|
||||
Type: filesandordirs; Name: "{app}\ChromeHeadlessShell\*"
|
||||
Type: filesandordirs; Name: "{app}\Chrome"
|
||||
Type: filesandordirs; Name: "{app}\ChromeHeadlessShell"
|
||||
|
||||
[Icons]
|
||||
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\launcher\ayanova-launcher.exe";
|
||||
@@ -68,11 +80,35 @@ Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\launcher\ayanova-launcher.e
|
||||
;removed this from above line to force desktop icon Tasks: desktopicon
|
||||
|
||||
[Run]
|
||||
; case 4579: the program folder is no longer writable by all local users (nothing writes there, data is under
|
||||
; {commonappdata}\ayanova). Upgrades over 8.2.4 or older still carry the old grant, so remove the Users group's own
|
||||
; rights (by SID, the group name is translated) from the program folder and everything in it; inherited read rights stay
|
||||
Filename: "{sys}\icacls.exe"; Parameters: """{app}"" /remove:g *S-1-5-32-545 /T /C /Q"; Flags: runhidden waituntilterminated
|
||||
Filename: "{app}\launcher\ayanova-launcher.exe"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
|
||||
|
||||
[Code]
|
||||
// case 4694: the launcher of 8.2.4 and older left the local PostgreSQL server running when AyaNova was shut down, so
|
||||
// its files are in use; stop it (if running) before installing over it or uninstalling
|
||||
procedure StopLocalPostgres();
|
||||
var
|
||||
PgCtl: String;
|
||||
ResultCode: Integer;
|
||||
begin
|
||||
PgCtl := ExpandConstant('{app}\local-postgres\bin\pg_ctl.exe');
|
||||
if FileExists(PgCtl) and DirExists(ExpandConstant('{commonappdata}\ayanova\database')) then
|
||||
Exec(PgCtl, 'stop -D "' + ExpandConstant('{commonappdata}\ayanova\database') + '" -m fast -w', ExtractFileDir(PgCtl), SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||
end;
|
||||
|
||||
function PrepareToInstall(var NeedsRestart: Boolean): String;
|
||||
begin
|
||||
StopLocalPostgres();
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
|
||||
begin
|
||||
if CurUninstallStep = usUninstall then
|
||||
StopLocalPostgres();
|
||||
if CurUninstallStep = usPostUninstall then
|
||||
begin
|
||||
if MsgBox('Do you want to delete all AyaNova database, attachment and log files?', mbConfirmation, MB_YESNO or MB_DEFBUTTON2) = IDYES then
|
||||
|
||||
4
dist/latest-version.json
vendored
4
dist/latest-version.json
vendored
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"latestversion":"8.2.4",
|
||||
"changelogurl":"https://ayanova.com/docs/changelog/#ayanova-823"
|
||||
"latestversion":"8.3.1",
|
||||
"changelogurl":"https://ayanova.com/docs/changelog/#ayanova-831"
|
||||
}
|
||||
BIN
dist/win-x64/postgres-standalone/bin/msvcp140.dll
vendored
Normal file
BIN
dist/win-x64/postgres-standalone/bin/msvcp140.dll
vendored
Normal file
Binary file not shown.
BIN
dist/win-x64/postgres-standalone/bin/vcruntime140.dll
vendored
Normal file
BIN
dist/win-x64/postgres-standalone/bin/vcruntime140.dll
vendored
Normal file
Binary file not shown.
BIN
dist/win-x64/postgres-standalone/bin/vcruntime140_1.dll
vendored
Normal file
BIN
dist/win-x64/postgres-standalone/bin/vcruntime140_1.dll
vendored
Normal file
Binary file not shown.
@@ -47,7 +47,7 @@ Service rates Cost and Price fields are **locked** to prevent editing once they
|
||||
|
||||
### Retiring
|
||||
|
||||
When a rate is retired it should be set to inactive by un-checking it's Active checkbox and a new rate created to take it's place. Rate names must be unique so for example if a rate changes yearly then the old rate might be called "In-shop service 2024" and the new rate called "In shop service 2025" to ensure names are unique and recognized by users.
|
||||
When a rate is retired it should be set to inactive by un-checking it's Active checkbox and a new rate created to take it's place. Rate names must be unique so for example if a rate changes yearly then the old rate might be called "In-shop service 2024" and the new rate called "In shop service 2026" to ensure names are unique and recognized by users.
|
||||
|
||||
### Contract service rates
|
||||
|
||||
|
||||
@@ -17,13 +17,13 @@ Full access
|
||||
|
||||
- Accounting
|
||||
- Business administration
|
||||
- Travel manager
|
||||
- Service manager
|
||||
|
||||
Read only access
|
||||
|
||||
- Business administration - restricted
|
||||
- Travel manager - restricted
|
||||
- Travel technician
|
||||
- Service manager - restricted
|
||||
- Service technician
|
||||
- Sales
|
||||
|
||||
## Import / Export
|
||||
|
||||
@@ -29,6 +29,10 @@ Logos are stored in the AyaNova database and appear in reports and on the login
|
||||
If you do not want to customize the login form and only want a logo for reporting then you would only need to provide the Small sized logo.
|
||||
If you provide a Medium or Large sized logo they will automatically be used by the Login form in place of the standard AyaNova logo.
|
||||
|
||||
### Logo file types
|
||||
|
||||
Logos must be PNG, JPEG, GIF, WebP or SVG images. Other files are refused when uploaded.
|
||||
|
||||
### Maximum logo file size
|
||||
|
||||
Maximum file size allowed for any logo is 512KiB (512000 bytes) however you should ensure your logo is as small a file size as possible while still maintaining visual quality. Under 100kb is a good rule of thumb.
|
||||
|
||||
@@ -22,11 +22,9 @@ Full access
|
||||
|
||||
- Business administration
|
||||
|
||||
- Business administration - restricted
|
||||
|
||||
Read only access
|
||||
|
||||
- none
|
||||
- Business administration - restricted
|
||||
|
||||
## How to access report templates
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ One side of this feature is the [Integrated applications User interface](adm-int
|
||||
|
||||
Using standard AyaNova API routes, developers can leverage this feature to provide configuration and mapping data persistance, logging for troubleshooting and a master on/off "switch" to control the active status of the integrating application.
|
||||
|
||||
An integrating application can sign in as a User with any of these full (not restricted) [roles](ay-biz-admin-roles.md) to create its integration record, read and update its data and write to its log: Business administration, Service manager, Inventory, Accounting, Service technician, Sales or System operations. Changing the Active status of an integration and deleting an integration are only available to the Business administration role, as in the [Integrated applications User interface](adm-integrations.md); the application should read the Active status and not change it.
|
||||
|
||||
### API Routes
|
||||
|
||||
As with all API routes in AyaNova, these API routes can be viewed in the [API explorer console](api-console.md).
|
||||
|
||||
@@ -31,7 +31,6 @@ We gratefully acknowledge the following open source tools and libraries that wer
|
||||
- [jose-jwt](https://github.com/dvsekhvalnov/jose-jwt)
|
||||
- [Json.NET](https://www.newtonsoft.com/json)
|
||||
- [MailKit](https://github.com/jstedfast/MailKit)
|
||||
- [MiniProfiler](https://github.com/MiniProfiler/dotnet)
|
||||
- [NLog](https://github.com/nlog/NLog/)
|
||||
- [Npgsql](https://www.npgsql.org/)
|
||||
- [PostgreSQL](https://www.postgresql.org/)
|
||||
|
||||
@@ -20,7 +20,7 @@ This section provides information regarding the AyaNova web app client running i
|
||||
|
||||
#### Version
|
||||
|
||||
This is the version number of the AyaNova web application. When a new version of AyaNova is released this number will go up. AyaNova uses the industry standard [semantic versioning system](https://semver.org/) which is 3 numbers separated by periods for example `8.2.4`.
|
||||
This is the version number of the AyaNova web application. When a new version of AyaNova is released this number will go up. AyaNova uses the industry standard [semantic versioning system](https://semver.org/) which is 3 numbers separated by periods for example `8.3.1`.
|
||||
|
||||
The leftmost number, for example **8**.1.2 is the major release version indicating major changes to the program that are not backwards compatible to the prior major release version.
|
||||
|
||||
|
||||
@@ -86,6 +86,12 @@ We have a [Guided tour for custom fields](tour-bizadmin-form-custom-fields.md) a
|
||||
|
||||
If you change the data type of a custom field after data has been entered AyaNova will do it's best to work with that change but it's not recommended. It's better to simply hide the former custom control and show a new one with the new data type.
|
||||
|
||||
#### Checkbox custom fields on existing records
|
||||
|
||||
When a `True / False` checkbox custom field is enabled, records that already existed have no value for it at all, neither true nor false. In a [data table](ay-start-form-data-tables.md) column for that field those records show nothing (an "indeterminate" value).
|
||||
|
||||
Once the checkbox is set or cleared on a record and the record is saved, the data table shows true or false for it like any other record.
|
||||
|
||||
### Changing the title of fields
|
||||
|
||||
To change the label text displayed to the User above any field (not just custom fields), edit any [translations](adm-translations.md) in use for that field's translation key. The internal Translation Key for each field is displayed in the Customize form beneath the current translation of that field's title to simplify finding which translation item needs to be changed.
|
||||
|
||||
@@ -60,7 +60,7 @@ The default login for the AyaNova SuperUser account is user name: `superuser` pa
|
||||
|
||||
Before AyaNova is licensed and used in production or before you import / migrate or enter any real world data into an evaluation AyaNova you **MUST** change the sample User passwords or anyone could potentially login and view your data.
|
||||
|
||||
AyaNova will prompt you to change it if it discovers a login using the default superuser password with a non evaluation license.
|
||||
With a non evaluation license, AyaNova requires it to be changed: a login using the default superuser password can only be used to change the password, until it is changed.
|
||||
|
||||
## Generating sample data
|
||||
|
||||
|
||||
@@ -176,6 +176,20 @@ Note that the SAMPLE DATA section of the user interface in the report designer s
|
||||
|
||||
A common use for the ayPrepareData function would be to insert or re-shape data based on calculations or data fetched from external API's or the AyaNova API itself (see the section regarding API usage below for details). It is defined as an asynchronous function so that the User can all AyaNova or other external API's if necessary to fetch data to insert into the report. For example getting part images from a manufacturers API site to insert into a report document.
|
||||
|
||||
#### Script errors
|
||||
|
||||
When a report fails to render, the error shown includes everything the report's scripts wrote with `console.error` (and `console.log` / `console.warn`). For `console.error`, objects passed to it are also shown as their contents and errors with their stack, for example:
|
||||
|
||||
```javascript
|
||||
try {
|
||||
// ...
|
||||
} catch (e) {
|
||||
console.error("Getting part images failed", e, { partId: item.PartId });
|
||||
}
|
||||
```
|
||||
|
||||
In a stack line such as `at ayPrepareData (<anonymous>:12:30)` the numbers are the line and column within your Prepare or Helpers script.
|
||||
|
||||
### Helpers
|
||||
|
||||

|
||||
@@ -304,8 +318,8 @@ ayClientMetaData:
|
||||
{
|
||||
"UserName": "AyaNova username",
|
||||
"UserId": "23",
|
||||
"Authorization": "Bearer eJhbGciO...token...data...hb9JyjcWl3Tib",
|
||||
"DownloadToken": "IslXKU....token....data...here....Bg2Vx0yT2rQ",
|
||||
"Authorization": "Bearer ay-report-render-token",
|
||||
"DownloadToken": "ay-report-render-download-key",
|
||||
"TimeZoneName": "America/Los_Angeles",
|
||||
"LanguageName": "en-US",
|
||||
"Hour12": true,
|
||||
@@ -351,7 +365,7 @@ ayServerMetaData:
|
||||
}
|
||||
```
|
||||
|
||||
- ayApiUrl - this is the URL of the server relative to the report generator and is useful for calling AyaNova API methods in your prepare data function to bring in sources of information from other areas of AyaNova that might not be directly available to your report
|
||||
- ayApiUrl - this is the URL of the server relative to the report generator and is useful for calling AyaNova API methods in your prepare data function to bring in sources of information from other areas of AyaNova that might not be directly available to your report. Always call the AyaNova API through this URL (or the API convenience functions below, which use it): the User's access is only added to requests made to this URL (see [API Usage](#api-usage))
|
||||
- HasXXLogo - these keys are set to true if a Logo has been uploaded for each size to AyaNova and are useful for controlling whether or not to show a logo on a report template as can be seen in many of our sample reports
|
||||
- Company XXX fields - these fields represent your company information as entered in Global settings to provide a centralized source of information for all reports requiring company contact or address data
|
||||
|
||||
@@ -559,9 +573,16 @@ The `ayGroupByKey` function does no sorting so the data will be returned in the
|
||||
|
||||
The full power of the [AyaNova developer's API](api-intro.md) is available to reports to incorporate data from anywhere in AyaNova as required.
|
||||
|
||||
The currently logged in User's API Bearer access token is provided for accessing API routes via the `ayClientMetaData.Authorization` value (see above).
|
||||
API calls from a report run with the access of the User running the report, so a report can only do what that User could also do in the AyaNova application. Reports attached to [customer notifications](customer-notify-subscriptions.md) run with full administrative access.
|
||||
|
||||
You should not store a static copy of an API Bearer access token or hard code it into your templates because it changes regularly and will expire the moment the User logs in to AyaNova.
|
||||
The report itself never holds the User's actual credentials. The `ayClientMetaData.Authorization` and `ayClientMetaData.DownloadToken` values are stand-ins, and the AyaNova server puts the User's actual access in their place only on requests made to the `ayApiUrl` address. So API calls and attachment images work as usual through `ayApiUrl`, but:
|
||||
|
||||
- an API call to any other address, including your AyaNova server's own public address typed into a template, is made without the User's access and will be refused; use `ayApiUrl` instead
|
||||
- a link to an attachment printed in the finished report (for example from a wiki) does not include a download key and will not open the attachment
|
||||
|
||||
Because a report template's script runs with the access of whoever runs the report, only Users with the full "Business administration" role can create or edit report templates.
|
||||
|
||||
You should not store a static copy of an API Bearer access token or hard code it into your templates.
|
||||
|
||||
While it is possible to login via a script using alternate credentials and access the API with alternative access to the current logged in user, this opens a security hole as you would need to hard code credentials into the report script and for this reason is **absolutely NOT recommended**.
|
||||
|
||||
|
||||
@@ -22,11 +22,7 @@ The password field has an option to show your password to make it easier to ensu
|
||||
|
||||
Click on the eye symbol to the right of the password field to show or hide the password entry.
|
||||
|
||||
AyaNova passwords and user names are case sensitive and there are no particular rules regarding passwords but longer passwords that are easier to remember without writing down are far more secure than shorter complicated ones with a mix of symbols that are hard to remember.
|
||||
|
||||
Using a mix of characters, numbers and symbols is unnecessary and outdated advice; modern security best practices emphasize simple text passwords that are easy to remember without having to write them down and as long as can be comfortably entered on your device.
|
||||
|
||||
For example this password `to see a world in a grain of sand` is easier to remember and many orders of magnitude more secure than this password `xJ#9a$a` due to it's length.
|
||||
AyaNova passwords and user names are case sensitive. AyaNova has no rules about password length or which characters a password must contain; see [What is a secure password?](home-password.md#what-is-a-secure-password) for how to choose a good one.
|
||||
|
||||
At the bottom of the login form is a link to the AyaNova website displayed as the version and copyright information for the AyaNova app.
|
||||
|
||||
@@ -36,7 +32,7 @@ The default login for the AyaNova SuperUser account is user name: `superuser` pa
|
||||
|
||||
Before AyaNova is licensed and used in production you **MUST** change this or anyone could login with this default.
|
||||
|
||||
AyaNova will prompt you to change it if it discovers a login using the default superuser password with a non evaluation license.
|
||||
With a non evaluation license, AyaNova requires it to be changed: a login using the default superuser password can only be used to change the password, until it is changed.
|
||||
|
||||
The built-in SuperUser User account in AyaNova is not a normal type of User and does not consume a license; it is used for specific special operations in AyaNova that could lead to loss of data or affect the operations of the server over all.
|
||||
|
||||
@@ -66,9 +62,7 @@ A User's authentication token can be [revoked at any time](adm-users.md#revoking
|
||||
|
||||
## Loss of authentication on server restart
|
||||
|
||||
If the Users are forced to re-login any time the AyaNova server is restarted the [JWT Secret setting](ops-config-jwt-secret.md) was probably not set as recommended during installation and should be verified.
|
||||
|
||||
If this setting is configured properly Users will be able to continue working after the server is restarted without the need to log back in again.
|
||||
Users stay logged in when the AyaNova server restarts, because the server keeps its [sign-in key](ops-config-jwt-secret.md) in its data folder. If Users are forced to log in again after every restart, check the server log at startup: AyaNova notes there if it could not read or create the sign-in key file, which usually means the AyaNova server cannot write to its data folder.
|
||||
|
||||
## Logout
|
||||
|
||||
|
||||
@@ -6,6 +6,83 @@ If you are viewing this page from your local copy of AyaNova be advised that it
|
||||
|
||||
See the [upgrade instructions](ops-upgrade.md) section of this manual for details.
|
||||
|
||||
## 2026
|
||||
|
||||
### AyaNova 8.3.1
|
||||
|
||||
Released 2026-11-05
|
||||
|
||||
**Upgrading from 8.2.4 or older?** AyaNova 8.3.0 and newer need a one-time [pre-upgrade step](ops-upgrade.md#upgrading-from-824-or-older) on Windows network and Linux installations.
|
||||
|
||||
**Changed**
|
||||
|
||||
- Linux: the [upgrade](ops-upgrade.md) scripts now first check that the .NET 10 runtime is installed, and stop without changing anything if it is not.
|
||||
|
||||
- Server: in some cases the automatic backup could be skipped for one day after the AyaNova server was restarted or a manual backup was made; it now always runs at the next chosen [backup time](ops-form-backup.md#backup-time).
|
||||
- Server: in some cases opening a record that has since been deleted (for example from an old link) gave a server error, and a report printed for records of which some had since been deleted failed; it now shows a "not found" message, and the report prints the remaining records.
|
||||
- Server: in some cases deleting a record that is still in use elsewhere (for example a customer, unit, part or rate used on a work order, quote or PM) gave a server error; it now always says which kind of record is using it.
|
||||
- Server: the server log now records the next automatic backup time (or that automatic backup is off) each time the server starts.
|
||||
- Backup: the [number of backups to keep](ops-form-backup.md#number-of-backups-to-keep) can no longer be set below 1; a setting of 0 already kept the most recent backup, the same as 1.
|
||||
- User interface: in some cases, when the web browser could not identify its time zone, dates and times were shown and entered in New York time; the browser's own offset from UTC is now used instead. A [time zone override](ay-start-localization.md#time-zone-override) in user options always takes precedence.
|
||||
- Email: report PDF files attached to customer notification emails now have the standard PDF content type; mail programs already opened them by their .pdf name.
|
||||
- Email: sending an email directly to a user who has no email address, or who is inactive, now names them as a user instead of a vendor in the message.
|
||||
|
||||
### AyaNova 8.3.0
|
||||
|
||||
Released 2026-10-29
|
||||
|
||||
**WARNING:** this release contains changes that will require a **pre upgrade step** on all Windows and Linux network platforms from the normal upgrade process you may be accustomed to. Be sure to follow our [upgrade instructions](ops-upgrade.md) and the **pre upgrade step** for your platform for this particular upgrade from version 8.2.4 (or older) to version 8.3.0 (or newer).
|
||||
|
||||
**IMPORTANT:** this release includes important security improvements along with the move to .NET 10. We recommend upgrading to 8.3.0 promptly.
|
||||
|
||||
**Changed**
|
||||
|
||||
- Server: significant performance improvements.
|
||||
- Server: after several incorrect two-factor authentication codes, the password must be entered again before another code can be tried.
|
||||
- Server: the `AYANOVA_JWT_SECRET` setting is no longer used. AyaNova now generates its own [sign-in key](ops-config-jwt-secret.md) once and keeps it in its data folder, so users stay logged in when the server restarts. The setting can be removed from `config.json` if desired.
|
||||
- Server: updated to use .NET 10.x LTS from previously used .NET 8.x LTS. .NET 8.x reaches end of support in November 2026. With this update to .NET 10.x come performance and security gains.
|
||||
- Server: all 3rd party components used by the AyaNova server have been upgraded to their most recent release versions to use with .NET 10.x. This improves performance, stability and security for each component used.
|
||||
- Server: the report rendering system now uses the smaller and faster "Chrome headless shell" browser instead of the full Chrome browser. It is downloaded into a `report-browser` folder under the AyaNova data folder instead of the program folder, so it is no longer removed by an upgrade. 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. Any old `Chrome` and `ChromeHeadlessShell` folders in the AyaNova program folder are no longer used and can be removed.
|
||||
- Server: the automatic internal backup now runs the database backup utility directly instead of through a command shell, so the database password can contain any characters and the [backup utility path](ops-config-backup-pgdump-path.md) can contain spaces, and a failed backup is now reported as an error instead of appearing to succeed in some cases. See [backup](ops-form-backup.md).
|
||||
- Server: log files are now named with an increasing number when they are archived; the [server log](ops-log.md) form lists them newest first.
|
||||
- Server: AyaNova now checks the email (SMTP) server's security certificate when sending email. If your mail server uses a self-signed or internal certificate and email notifications stop after upgrading, see [SMTP certificate check](ops-config-smtp-accept-any-certificate.md).
|
||||
- Server: after repeated failed login attempts, further attempts for that login, or from that address, are refused for a few minutes.
|
||||
- Server: new [trusted reverse proxies](ops-config-trusted-proxies.md) setting for a reverse proxy that runs on a different computer from the AyaNova server, so AyaNova sees each user's own address.
|
||||
- Server: while a report renders, its template no longer holds the User's actual access token or download key; the AyaNova server adds the User's access only to the report's requests to the `ayApiUrl` address. Templates that call the AyaNova API through any other address need to be changed. See [API usage in reports](ay-report-edit.md#api-usage).
|
||||
- Server: users with the "Business administration - restricted" role can view [report templates](adm-report-templates.md) but no longer create, edit or import them, as described for that [role](ay-biz-admin-roles.md). The full "Business administration" role is required for users who need to edit report templates.
|
||||
- Server: relative date filters such as "today" or "last month" (in lists and dashboard widgets) now start and end the period at midnight in the user's own [time zone](ay-start-form-data-tables.md#filtering-dates-and-time-zones). Previously, for a user in a different time zone from the server, records close to the start or end of the period could be included or left out in some cases.
|
||||
- Server: when a report fails to render, objects and errors a report script passes to `console.error` are now shown with their contents and stack (line numbers within the script) in the error message, to help with [writing report scripts](ay-report-edit.md#script-errors).
|
||||
- Server: on servers in a time zone with daylight saving time, the automatic backup could run an hour earlier or later than the chosen [backup time](ops-form-backup.md#backup-time) for part of the year; it now always runs at the chosen time on the server's clock.
|
||||
- Server: a work order created from a [customer service request](svc-csr.md#accept-to-new-work-order) can now be deleted; the request goes back to Open. Previously the delete failed with a server error.
|
||||
- Server: the SuperUser account can no longer be deleted. Previously it could be deleted on a new installation before it had any activity.
|
||||
- Server: role rights corrected to match the documentation: "Service technician - restricted" users can view tax codes, projects, unit models and meter readings; "Business administration" users can view server metrics; Accounting users can no longer enter unit meter readings; only "Business administration" users can switch an [integrated application](api-integrated-applications.md) on or off or delete it.
|
||||
- Server: fixed direct notification messages to a user: in some cases only the first one within 12 hours was delivered, now every message is.
|
||||
- Server: the server information page at `/api/v8/` no longer shows license details.
|
||||
- Server: business logo uploads must be PNG, JPEG, GIF, WebP or SVG images, and require full rights to global settings. See [business logos](adm-global-logo.md).
|
||||
- Server: when a user's password is reset or set by an administrator, or their login is disabled, they are signed out and must sign in again.
|
||||
- Server: on a server with a purchased license, the SuperUser account can only be used to change its password if it still has the default password, and the login form no longer offers the default SuperUser login. See [SuperUser login](ay-start-login.md).
|
||||
- Server: the SMTP server password is no longer shown in the [notification settings](ops-notification-system.md) form; it shows `********` when a password is set. Type a new password to change it.
|
||||
- Linux: Ubuntu 24.04 and 26.04 are now tested and supported. On Ubuntu 24.04 and newer the .NET runtime is installed from Ubuntu's own package archive.
|
||||
- Docs: updated the [Linux server](ops-install-linux-server.md), [Linux desktop](ops-install-linux-desktop.md), [Windows local network](ops-install-windows-lan.md) and [Windows IIS](ops-install-windows-iis.md) installation and upgrade instructions for .NET 10 and Ubuntu 24.04 / 26.04.
|
||||
- Docs: updated the database password guidance in the installation and [backup](ops-form-backup.md) documentation.
|
||||
- Windows IIS: reports, including reports attached to customer notifications, now render under IIS with any site bindings, HTTPS included, without the [report rendering API URL override](ops-config-report-rendering-api-url-override.md) setting. See [report rendering under IIS](ops-install-windows-iis.md#report-rendering-under-iis).
|
||||
- Windows single user: the database server now shuts down when AyaNova is shut down, so installing an upgrade no longer stops at a "files in use" page. The installer also shuts down a database server left running by an earlier version.
|
||||
- Windows single user: the Microsoft Visual C++ runtime files the built-in database server needs are now included, so it no longer fails to start on computers without them.
|
||||
- Windows: the installers no longer give all local users write access to the AyaNova program folder; only administrators can change files there. Upgrading removes the earlier write access.
|
||||
- Windows: the local network installer and the [local network](ops-install-windows-lan.md) and [IIS](ops-install-windows-iis.md) installation instructions now link to the official PostgreSQL download page (the third-party guide linked before is no longer available), and the configuration examples assume PostgreSQL 18.
|
||||
- Docs: the Windows installation instructions now state that only one type of AyaNova 8 installation (single user, local network or IIS) can be on a computer at a time.
|
||||
|
||||
**Fixed**
|
||||
|
||||
- Server: fixed occasional errors on the first requests after the server starts.
|
||||
- Server: searching selection lists for text that contains an apostrophe (for example a name like O'Brien) now works.
|
||||
- Server: hardened data table filter handling of number and role filter values.
|
||||
- Server: data table date filters for "greater than or equal", "less than" and "not equal" could include or exclude records on the boundary of the selected date.
|
||||
- Server: the database connection used for administrative operations is now built correctly when the database name also appears elsewhere in the connection setting.
|
||||
- Server: erasing the database with AYANOVA_PERMANENTLY_ERASE_DATABASE now checks that the database user has the rights to re-create the database before dropping it.
|
||||
- Server: if the report rendering browser cannot be downloaded or started, a specific error is now shown and logged instead of a generic job failure.
|
||||
- Docs: fixed several links in the manual that opened the wrong section or page.
|
||||
|
||||
## 2025
|
||||
|
||||
### AyaNova 8.2.4
|
||||
@@ -309,7 +386,7 @@ Released 2023-05-10
|
||||
|
||||
**Added**
|
||||
|
||||
- UI: [Service schedule](svc-schedule.md#create--modify--view-events) added Customer selection / edit link to new work order dialog to preselect Customer or create new one using [Open record button](ay-start-form-autocomplete.md#opening-selected-record)
|
||||
- UI: [Service schedule](svc-schedule.md#create-modify-view-events) added Customer selection / edit link to new work order dialog to preselect Customer or create new one using [Open record button](ay-start-form-autocomplete.md#opening-selected-record)
|
||||
- UI: All schedule forms click on Work order more info dialog added [Work order header's Summary text](svc-workorders.md#summary) below work order serial and customer name
|
||||
|
||||
### AyaNova 8.0.26 (2022-11-28)
|
||||
|
||||
@@ -26,7 +26,7 @@ In addition customer notifications will attempt to send attached reports if sele
|
||||
|
||||
On the other hand if you enable Customers to set up their _own_ notifications via a Customer login account, then _they_ are in control and can choose what they get, when they get it and how they get it. When they want to view a work order or other report they can simply view it directly in the AyaNova web application and download or save it if they choose without any email provider being involved.
|
||||
|
||||
The regular AyaNova notification system used for self subscribers ensures deliberately short notices with simple URL links to login and view data intended to work well with automated spam filtering and suitable for not only emailing but sending as [SMS text](home-notify-subscriptions.md#sms--text-delivery) or through gateways to [other platforms such as Slack](home-notify-subscriptions.md#slack-and-other-communication-services).
|
||||
The regular AyaNova notification system used for self subscribers ensures deliberately short notices with simple URL links to login and view data intended to work well with automated spam filtering and suitable for not only emailing but sending as [SMS text](home-notify-subscriptions.md#sms-text-delivery) or through gateways to [other platforms such as Slack](home-notify-subscriptions.md#slack-and-other-communication-services).
|
||||
|
||||
In addition there are numerous other features available to [customer login accounts](adm-global-settings.md#customer-access-settings) including the ability to request service from within AyaNova and features specifically to support [Head offices](head-offices.md) that you may find helpful.
|
||||
|
||||
|
||||
@@ -18,21 +18,14 @@ You can click on the eye icon to conceal your entry from onlookers.
|
||||
|
||||
### What is a secure password?
|
||||
|
||||
#### Long, not complicated
|
||||
#### Randomly chosen
|
||||
|
||||
Modern security industry best practices around passwords focus entirely on length and not 'complexity' and AyaNova fully supports this by allowing very lengthy passwords and not requiring any special characters.
|
||||
What makes a password hard to guess is how randomly it was chosen, not only its length. Password guessing tools try well known phrases first, such as song lyrics, movie quotes and lines from books and poems, along with common substitutions such as `0` for `o` or `1` for `i`. A long password made from a phrase you picked because it is memorable can be guessed far faster than its length suggests.
|
||||
|
||||
For an adequate level of security ensure your password is **at minimum** 15 characters long or as long as you can comfortably remember without writing it down.
|
||||
AyaNova accepts passwords of any length and has no rules about which characters they must contain, so you can use either of these:
|
||||
|
||||
#### Memorable
|
||||
|
||||
We recommend using a lengthy but easily remembered phrase as a password, for example a song lyric, movie quote or a line from a poem that is **at least** 15 characters or more of words is currently _extremely_ difficult to hack by brute force methods.
|
||||
|
||||
There is no security advantage in modern practice to using mixed case, unusual symbols or numbers in your password as brute force hacking techniques try all enterable characters anyway and it just makes the password harder to remember.
|
||||
|
||||
It is more secure to have a lengthy password of plain alphabet text than it is to have a shorter one with a mix of numbers and characters.
|
||||
|
||||
For example: this password `somewhereovertherainbowskiesareblue` is far more secure than this password `Ab3#@%5` and easier to remember without writing it down.
|
||||
- **A password manager** (recommended): let it generate a long random password and remember it for you.
|
||||
- **A passphrase of at least six random words**: for example roll dice to pick words from the [EFF long word list](https://www.eff.org/files/2016/07/18/eff_large_wordlist.txt). Six words chosen this way are easy to type and remember and very hard to guess, as long as the dice choose the words, not you.
|
||||
|
||||
#### Impersonal
|
||||
|
||||
@@ -46,7 +39,7 @@ In the current climate of hacking and malware we recommend you do _not_ enter yo
|
||||
|
||||
#### Backed up by Two factor authentication
|
||||
|
||||
AyaNova has [built in support](home-tfa.md) for the current most secure form of Two factor authentication and we encourage you to use it for all users.
|
||||
AyaNova has [built in support](home-tfa.md) for Two factor authentication and we encourage you to use it for all users.
|
||||
|
||||
In particular any User account that has the Business administration role (such as the built in SuperUser account) should be backed up with with two factor authentication codes as these roles are extremely powerful and can be used to easily circumvent security by assigning roles to other Users or creating entirely new Users.
|
||||
|
||||
|
||||
@@ -18,9 +18,7 @@ You may be viewing an older local version of this manual that was installed with
|
||||
|
||||
The most recent manual can always be viewed on our website at [https://www.ayanova.com/docs/](https://www.ayanova.com/docs/)
|
||||
|
||||
You can tell which version of the manual you are viewing by look at the revision stamp at the bottom of the page which shows the year month and day that copy of the manual was last updated:
|
||||
|
||||
`REV-YYYY-MM-DD `
|
||||
You can tell which version of the manual you are viewing by looking at the revision stamp at the bottom of the page: `REV-` followed by the year, month and day this copy of the manual was last changed.
|
||||
|
||||
### Beyond this manual
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ Ground Zero will comply with all applicable laws pertaining to personal data inc
|
||||
|
||||
A: Data obtained
|
||||
|
||||
Information about you (such as your email address, name, company information) is provided to us when you email us (such as when you email us directly, email us via our online form, email us via our online temporary Key request form) or when you register on our AyaNova Support Forum.
|
||||
Information about you (such as your email address, name, company information) is provided to us when you email us (such as when you email us directly, email us via our online form, email us via our online temporary Key request form).
|
||||
|
||||
Information you provided in your order (such as your company name, company contact information, contact name, along with the AyaNova products/services/subscriptions purchased) is sent to us from our payment processing partner with whom you placed your order.
|
||||
|
||||
@@ -61,7 +61,7 @@ Ground Zero will not share your information for marketing purposes with companie
|
||||
|
||||
C: Use and Sharing of Data
|
||||
|
||||
Personal data you provided when you contact us (such as when you email us directly, email us via our online form, email us via our online temporary Key request form or register on our AyaNova Support Forum) will be used to provide technical support, to respond to user inquiries, to provide temporary Key(s), to maintain case and bug issues reported by you, and to send you relevant AyaNova information.
|
||||
Personal data you provided when you contact us (such as when you email us directly, email us via our online form, or email us via our online temporary Key request form) will be used to provide technical support, to respond to user inquiries, to provide temporary Key(s), to maintain case and bug issues reported by you, and to send you relevant AyaNova information.
|
||||
|
||||
Ground Zero will also receive your company information you provided to our payment processing partner, so that we may process your product/service/subscription order.
|
||||
|
||||
@@ -83,8 +83,7 @@ E: Deleting of Data
|
||||
(i) Temporary key requests without subsequent purchase will be deleted within one year.
|
||||
(ii) Email correspondence without subsequent purchase will be deleted within two years except where required for maintaining case and support history.
|
||||
(iii) Email correspondence including temporary key requests with subsequent subscription purchase will be retained for the life of your subscription. Once a subscription has been allowed to lapse (not renewed), correspondence will be deleted within two years except where required for maintaining case and support history.
|
||||
(iv) Customer information obtained through AyaNova Support Forum registration will be deleted within three years of forum login inactivity.
|
||||
(v) Purchase information along with company information as provided by our payment processing partner will be retained for a minimum of six years after the end of the taxation year to which they relate as per the Canada Revenue Agency and no longer than 10 years after which will be deleted within one year.
|
||||
(iv) Purchase information along with company information as provided by our payment processing partner will be retained for a minimum of six years after the end of the taxation year to which they relate as per the Canada Revenue Agency and no longer than 10 years after which will be deleted within one year.
|
||||
|
||||
|
||||
I HAVE READ AND UNDERSTOOD ALL OF THE ABOVE TERMS AND CONDITIONS AND ACKNOWLEDGE THAT THESE TERMS AND CONDITIONS FORM PART OF THIS AGREEMENT. I AGREE THAT A PRINT-OUT OF THIS AGREEMENT WILL CONSTITUTE A "WRITING" UNDER ANY APPLICABLE LAWS.
|
||||
|
||||
@@ -31,7 +31,7 @@ Example WINDOWS config.json entry
|
||||
```json
|
||||
{
|
||||
...other properties...
|
||||
"AYANOVA_BACKUP_PG_DUMP_PATH": "C:\\Program Files\\PostgreSQL\\16\\bin"
|
||||
"AYANOVA_BACKUP_PG_DUMP_PATH": "C:\\Program Files\\PostgreSQL\\18\\bin"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -43,7 +43,7 @@ Example environment variable
|
||||
|
||||
Windows
|
||||
|
||||
`set "AYANOVA_BACKUP_PG_DUMP_PATH=C:\Program Files\PostgreSQL\16\bin"` (not real)
|
||||
`set "AYANOVA_BACKUP_PG_DUMP_PATH=C:\Program Files\PostgreSQL\18\bin"` (not real)
|
||||
|
||||
Linux
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ If no default database is specified AyaNova will use the default value: "AyaNova
|
||||
|
||||
## Special characters could cause backup to fail
|
||||
|
||||
Be careful about using special characters for your Postgres connection string, particularly the password and database name, we've encountered issues with the automatic internal backup failing due to a @ or $ symbol in the password. On some platforms when AyaNova attempts to run the backup utility it may fail if that operating system does not support certain special characters found in the password as they may interfere with the "shell" command called to perform the backup.
|
||||
From AyaNova 8.3.0 the automatic internal backup passes the database password to the backup utility directly, without a command shell, so any characters can be used in the PostgreSQL password and database name. Versions older than 8.3.0 could fail to back up if the password contained characters such as `@`, `$`, `:`, `/`, `?`, `#`, `%` or spaces; if you are on an older version, upgrade or avoid those characters.
|
||||
|
||||
## Optional connection string parameters
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ Command line parameters are appended to the command to start the ayanova server,
|
||||
|
||||
### API
|
||||
|
||||
- [AYANOVA_JWT_SECRET](ops-config-jwt-secret.md)
|
||||
- [AYANOVA_JWT_SECRET](ops-config-jwt-secret.md) (no longer used as of version 8.3.0)
|
||||
- [AYANOVA_USE_URLS](ops-config-use-urls.md)
|
||||
|
||||
### REPORTING
|
||||
@@ -95,6 +95,11 @@ Command line parameters are appended to the command to start the ayanova server,
|
||||
- [AYANOVA_REPORT_RENDERING_TIMEOUT](ops-config-report-rendering-timeout.md)
|
||||
- [AYANOVA_REPORT_RENDER_API_URL_OVERRIDE](ops-config-report-rendering-api-url-override.md)
|
||||
|
||||
### EMAIL
|
||||
|
||||
- [AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE](ops-config-smtp-accept-any-certificate.md)
|
||||
|
||||
### SECURITY
|
||||
|
||||
- [AYANOVA_SET_SUPERUSER_PW](ops-config-set-superuser-pw.md)
|
||||
- [AYANOVA_TRUSTED_PROXIES](ops-config-trusted-proxies.md)
|
||||
|
||||
@@ -1,56 +1,23 @@
|
||||
# JWT secret setting
|
||||
# Security and JWT web tokens
|
||||
|
||||
AyaNova uses JSON Web Tokens (JWT) for authentication.
|
||||
|
||||
These time limited tokens are signed by the server using a JWT secret key and issued to users when they log in to the AyaNova server.
|
||||
When a User logs in, the AyaNova server issues them a time limited token signed with the server's own sign-in key. The token is sent with every request the User makes and is verified by the server each time.
|
||||
|
||||
Every time the user makes a request to the server the JWT is sent along as well and verified to be valid.
|
||||
Tokens expire 5 days after they are issued, and each User can only have one current token: logging in again replaces the previous one.
|
||||
|
||||
Tokens have a built in expiry mechanism of 5 days from issue.
|
||||
(Individual Users can be prevented from logging in or accessing the server by [setting them inactive](adm-users.md#revoking-access-and-locking-out-users), which takes effect immediately regardless of how many days are left on their current token.)
|
||||
|
||||
All active tokens previously issued can be invalidated by changing this JWT Secret setting and restarting the server (or restarting the server and allowing it to choose a new secret value randomly if none is specified).
|
||||
## The sign-in key
|
||||
|
||||
(Individual Users can be prevented from logging in or accessing the server by setting them inactive which takes effect immediately regardless of how many days are left on their current access token.)
|
||||
The AyaNova server generates its sign-in key automatically the first time it starts and keeps it in a file named `jwt-signing.key` in the AyaNova [data folder](ops-config-data-path.md). It is used from then on, so Users stay logged in when the server restarts. There is nothing to configure.
|
||||
|
||||
## Default
|
||||
Keep the data folder protected like the rest of AyaNova's data: anyone who can read the sign-in key file could create valid tokens.
|
||||
|
||||
If no secret key is specified the server will generate a new, random one each time it starts and this means that remote users who previously authenticated will need to login freshly if the server is restarted.
|
||||
## Signing everyone out
|
||||
|
||||
If you would like to ensure that a server reboot does not affect remote users credentials then you can specify a value for the secret key so that the same key will always be used by the server even if it reboots.
|
||||
|
||||
## Overriding
|
||||
|
||||
AyaNova expects the JWT secret to be provided by a config.json property, environment variable or command line parameter named
|
||||
|
||||
`AYANOVA_JWT_SECRET`
|
||||
|
||||
The value specified should be a string of up to 32 characters, for example:
|
||||
`02847This_is_my_secret_key456576`
|
||||
If fewer than 32 characters are provided they secret will be padded out to 32 characters. If more than 32 characters are specified it will only use the first 32.
|
||||
|
||||
You should use the same precautions as for choosing any other password and ensure the secret is not well known or easily looked up in a dictionary. 32 random characters would be sufficient.
|
||||
|
||||
Example config.json entry
|
||||
|
||||
```json
|
||||
{
|
||||
...other properties...
|
||||
"AYANOVA_JWT_SECRET": "My32CharacterSecretKey0123456789"
|
||||
}
|
||||
```
|
||||
|
||||
Example command line parameter
|
||||
|
||||
`ayanova.exe --AYANOVA_JWT_SECRET="02847This_is_my_secret_key456576"`
|
||||
|
||||
Example environment variable
|
||||
|
||||
Windows
|
||||
|
||||
`set "AYANOVA_JWT_SECRET=02847This_is_my_secret_key456576"`
|
||||
|
||||
Linux
|
||||
|
||||
`export AYANOVA_JWT_SECRET="02847This_is_my_secret_key456576"`
|
||||
To invalidate every token that has been issued (for example if you suspect a token or the key file was exposed): stop the AyaNova server, delete the `jwt-signing.key` file from the data folder, and start the server again. A new key is generated and every User will need to log in again.
|
||||
|
||||
## AYANOVA_JWT_SECRET (no longer used)
|
||||
|
||||
Before version 8.3.0 the sign-in key could be set with an `AYANOVA_JWT_SECRET` configuration setting. That setting is no longer used: if it is still present in config.json, an environment variable or the command line it is ignored, a note is written to the server log, and it can safely be removed.
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
This optional setting sets the _full_ path to the Chrome web browser executable that is used by AyaNova to render reports.
|
||||
|
||||
If not set, AyaNova will attempt to automatically download a copy of Chrome to a `Chrome` and `ChromeHeadlessShell` folders under the AyaNova executable folder on first report rendering.
|
||||
If not set, AyaNova will attempt to automatically download a copy of Chrome to `Chrome` and `ChromeHeadlessShell` folders inside a `report-browser` folder under the [data path](ops-config-data-path.md) on first report rendering. The data folder must allow programs to run from it (on Linux it must not be on a `noexec` mount).
|
||||
|
||||
Versions before 8.3.0 downloaded Chrome to the AyaNova program folder instead. Those `Chrome` and `ChromeHeadlessShell` folders in the program folder are no longer used and can be deleted.
|
||||
|
||||
This setting is typically not required to be set.
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
The report rendering api override setting is used in situations where report rendering code is unable to contact the local AyaNova api server via it's localhost address.
|
||||
|
||||
**IIS:** as of AyaNova 8.3.0 this setting is no longer needed when AyaNova runs inside IIS. AyaNova sends the report's requests to the AyaNova server itself, to the same address and site name users connect to (see [report rendering under IIS](ops-install-windows-iis.md#report-rendering-under-iis)). A value set for an earlier version does no harm and can be removed.
|
||||
|
||||
The symptom of this issue is reports failing to render with an error displayed containing text similar to "JobFailed: PuppeteerSharp -> Evaluation failed: TypeError: Failed to fetch at ayPostToAPI"
|
||||
|
||||
Reports are rendered by a "headless" web browser running at the AyaNova server which executes the javascript required to render the report. Most report scripts will need to contact the AyaNova API to fetch data like translations or logos. In some scenarios such as some specific configurations when using IIS as the front end for AyaNova the local headless browser is prevented from contacting the AyaNova server due to accessing it via the `localhost` address. In these situations it's necessary to tell the script to use an alternative address.
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# SMTP certificate check setting
|
||||
|
||||
When AyaNova connects to the SMTP server set in the [notification settings](ops-notification-system.md) to send email, it checks the mail server's security certificate. The certificate must be issued by a trusted certificate authority and must match the SMTP server address entered in the notification settings. This prevents someone on the network from posing as your mail server to collect the SMTP account password and the messages being sent.
|
||||
|
||||
Most mail servers and email providers have a certificate that passes this check and nothing needs to be done.
|
||||
|
||||
## When the check fails
|
||||
|
||||
If the mail server's certificate can not be validated, email notifications are not sent. The Send test message option in the notification settings shows an error such as "The server's SSL certificate could not be validated", and failed deliveries are reported in the [notification system](ops-notification-system.md) as usual.
|
||||
|
||||
This can happen when:
|
||||
|
||||
- the mail server uses a self-signed certificate or one issued by your organization's own internal certificate authority
|
||||
- the SMTP server address is entered as an IP address or a name that the certificate does not cover
|
||||
|
||||
The best fix is to correct the cause: enter the SMTP server address exactly as named in its certificate, or install your internal certificate authority as trusted on the computer running the AyaNova server. If neither is possible, the check can be switched off with this setting.
|
||||
|
||||
## Default
|
||||
|
||||
If this setting is not specified the certificate is checked.
|
||||
|
||||
## Overriding
|
||||
|
||||
AyaNova expects this value to be provided by a config.json property, environment variable or command line parameter named
|
||||
|
||||
`AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE`
|
||||
|
||||
Set it to `true` to accept any certificate from the mail server. Only do this for a mail server on a network you trust: without the check, the connection to the mail server is encrypted but AyaNova can not tell if it is talking to your real mail server.
|
||||
|
||||
When it is set, the AyaNova server log records a warning each time the server starts.
|
||||
|
||||
Example config.json entry
|
||||
|
||||
```json
|
||||
{
|
||||
...other properties...
|
||||
"AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE": "true"
|
||||
}
|
||||
```
|
||||
|
||||
Example command line parameter
|
||||
|
||||
`ayanova.exe --AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE="true"`
|
||||
|
||||
Example environment variable
|
||||
|
||||
Windows
|
||||
|
||||
`set "AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE=true"`
|
||||
|
||||
Linux
|
||||
|
||||
`export AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE="true"`
|
||||
56
docs/8.0/ayanova/docs/ops-config-trusted-proxies.md
Normal file
56
docs/8.0/ayanova/docs/ops-config-trusted-proxies.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Trusted reverse proxies setting
|
||||
|
||||
When AyaNova is reached through a reverse proxy server (for example NGINX, Apache, IIS, a load balancer or a cloud service in front of AyaNova), every connection AyaNova receives comes from the proxy, not from the user's computer. The proxy passes along the user's real address in an `X-Forwarded-For` header.
|
||||
|
||||
AyaNova uses the user's address to limit repeated failed login attempts and to record where logins come from in the [server log](ops-log.md). Because anyone can add an `X-Forwarded-For` header to a request, AyaNova only accepts it from a proxy it trusts.
|
||||
|
||||
A reverse proxy running on the **same computer** as the AyaNova server and connecting to it through `localhost` or `127.0.0.1` is always trusted. This is how our [Linux server](ops-install-linux-server.md) and [Windows IIS](ops-install-windows-iis.md) installation instructions set it up, and nothing needs to be done.
|
||||
|
||||
## When this setting is needed
|
||||
|
||||
If the reverse proxy runs on a **different computer** from the AyaNova server, AyaNova does not trust it by default. All users then appear to come from the proxy's address, and they share a single limit on failed login attempts: repeated failed logins by one person can then refuse logins for everyone connecting through the proxy for a few minutes.
|
||||
|
||||
When this happens, the AyaNova server log records a warning once after each server start, naming the address of the proxy:
|
||||
|
||||
`A request came through a reverse proxy at 10.0.0.5 that is not trusted...`
|
||||
|
||||
If that address is your reverse proxy, add it to this setting.
|
||||
|
||||
## Default
|
||||
|
||||
If this setting is not specified only a reverse proxy on the same computer is trusted.
|
||||
|
||||
## Overriding
|
||||
|
||||
AyaNova expects this value to be provided by a config.json property, environment variable or command line parameter named
|
||||
|
||||
`AYANOVA_TRUSTED_PROXIES`
|
||||
|
||||
The value is a list of the IP addresses of your reverse proxies, separated by commas. A whole network can be given in CIDR notation, for example `10.0.0.0/24` for the addresses `10.0.0.0` to `10.0.0.255`. If there is more than one proxy in a chain (for example a load balancer in front of a reverse proxy), list each of them.
|
||||
|
||||
Only list addresses of your own proxy servers. Any computer listed here can tell AyaNova a user's address, so never list an address range that ordinary users connect from.
|
||||
|
||||
An entry that is not a valid IP address or network is ignored and the AyaNova server log records a warning when the server starts.
|
||||
|
||||
Example config.json entry
|
||||
|
||||
```json
|
||||
{
|
||||
...other properties...
|
||||
"AYANOVA_TRUSTED_PROXIES": "10.0.0.5, 10.0.1.0/24"
|
||||
}
|
||||
```
|
||||
|
||||
Example command line parameter
|
||||
|
||||
`ayanova.exe --AYANOVA_TRUSTED_PROXIES="10.0.0.5, 10.0.1.0/24"`
|
||||
|
||||
Example environment variable
|
||||
|
||||
Windows
|
||||
|
||||
`set "AYANOVA_TRUSTED_PROXIES=10.0.0.5, 10.0.1.0/24"`
|
||||
|
||||
Linux
|
||||
|
||||
`export AYANOVA_TRUSTED_PROXIES="10.0.0.5, 10.0.1.0/24"`
|
||||
@@ -8,7 +8,11 @@ If no override is specified AyaNova will use the following default value:
|
||||
|
||||
`"http://*:7575"`
|
||||
|
||||
This means AyaNova will listen on port 7575
|
||||
This means AyaNova will listen on port 7575 on all of the computer's network addresses.
|
||||
|
||||
## Behind a reverse proxy
|
||||
|
||||
When all users connect through a reverse proxy (such as NGINX) on the same computer, use `127.0.0.1` in place of `*`, for example `"http://127.0.0.1:7575"`. AyaNova then accepts connections only from that computer, so it can not be reached directly over unencrypted HTTP, bypassing the reverse proxy. See the [Linux server installation](ops-install-linux-server.md#f-accept-connections-only-through-nginx) instructions.
|
||||
|
||||
## Overriding
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ Other indications of a failed backup are unsually small backup files or errors i
|
||||
|
||||
The only **certain** way to know if a backup is good is to test restore it from time to time and we recommend doing so on a regularly scheduled basis to be certain the backup is good if you need it.
|
||||
|
||||
**Warning**: Be careful about using special characters for your Postgres connection string, particularly the password and database name, we've encountered issues with the automatic internal backup failing due to a @ or $ symbol in the password. On some platforms when AyaNova attempts to run the backup utility it may fail if that operating system does not support certain special characters found in the password as they may interfere with the "shell" command called to perform the backup.
|
||||
**Note**: From AyaNova 8.3.0 the automatic internal backup passes the database password to the backup utility directly, without a command shell, so any characters can be used in the PostgreSQL password and database name. Versions older than 8.3.0 could fail to back up if the password contained characters such as `@`, `$`, `:`, `/`, `?`, `#`, `%` or spaces; if you are on an older version, upgrade or avoid those characters.
|
||||
|
||||
## Best practices for disaster recovery
|
||||
|
||||
@@ -140,6 +140,8 @@ These defaults should be examined carefully and set to values that are appropria
|
||||
|
||||
Choose a time for the automatic backup when the server will not be in use by Users.
|
||||
|
||||
The backup runs at this time on the AyaNova server's own clock, every day, including across daylight saving time changes, so make sure the server's operating system is set to the correct time zone. The time is entered and shown in your own time zone, so if you are in a different time zone from the server, the time you see is the matching time in your time zone.
|
||||
|
||||
**Windows server IIS Warning** if you are hosting AyaNova via IIS on Windows be aware that there is a setting in IIS that will regularly restart AyaNova automatically and it must be adjusted to not conflict with the backup time and general usage.
|
||||
|
||||
If you're not sure then double check the IIS installation instructions [Application Pool settings](ops-install-windows-iis.md#74-edit-application-pool).
|
||||
@@ -156,7 +158,7 @@ Checking this box will cause AyaNova to also back up the attached files in addit
|
||||
|
||||
_AyaNova Subscription service keeps one backup by default and this option is not available_
|
||||
|
||||
The backup system will automatically remove old backups depending upon this setting. If it is set to 3 then the most recent three backups are kept.
|
||||
The backup system will automatically remove old backups depending upon this setting. If it is set to 3 then the most recent three backups are kept. The minimum is 1: the most recent backup is always kept.
|
||||
|
||||
If you are following the disaster recovery best practice policy of 10 backup rotation as [suggested above](#10-backup-rotation) then you would only need to keep 1 current backup set.
|
||||
|
||||
|
||||
@@ -84,13 +84,26 @@ Be sure to have all staff carefully test all business processes they require in
|
||||
|
||||
### AyaNova 8 database will be erased
|
||||
|
||||
The V8Migrate plugin will erase **all** user entered data including attached files in the AyaNova 8 database as the first step with the following exceptions:
|
||||
As its first step the V8Migrate plugin erases the AyaNova 8 database so it can be filled with your AyaNova 7 data.
|
||||
|
||||
**Erased** (replaced by the data migrated from AyaNova 7):
|
||||
|
||||
- All business data: Customers, Head offices, Customer contacts, Vendors, Units and Unit models, Parts and inventory, Purchase orders, Work orders, Quotes, Preventive maintenance, Projects, Contracts, Customer service requests, Loaners, Memos, Reminders, Reviews
|
||||
- Business settings entered as records: Service rates, Travel rates, Tax codes, Work order and Quote statuses, Work order item statuses and priorities, Task groups
|
||||
- All Users except the SuperUser account
|
||||
- Attached files, Tags, custom fields set up with [Customize form](ay-customize.md), saved data table filters and column views, autocomplete templates
|
||||
- Notification subscriptions and notifications, history, dashboard layouts, custom (non stock) translations and data stored by integrated applications such as QBI
|
||||
|
||||
**Kept**:
|
||||
|
||||
- The SuperUser account and its password
|
||||
- Global settings
|
||||
- Operations backup settings
|
||||
- Operations notification settings
|
||||
- Operations notification settings (including the email server settings)
|
||||
- Business logos
|
||||
- Reports (any missing stock report templates will be re-installed)
|
||||
- The stock translations
|
||||
- The license
|
||||
|
||||
There is no ability to synchronize or partially migrate only recent changes from v7; the migrate process entirely replaces the V8 data each time it's run.
|
||||
|
||||
@@ -216,7 +229,8 @@ Do not login to AyaNova 8 until migration has completed or stopped due to error
|
||||
- Select the "V8 Migrate" plugin in AyaNova 7 from the main window plugin menu and the login and server URL dialog will open
|
||||
- Enter the URL of your AyaNova 8 server. The easiest way to get this is to simply copy the entire URL displayed in the browser from the AyaNova 8 web application web page, for example the login page.
|
||||
- There is a "Test URL" button to confirm if you have entered the correct address or not.
|
||||
- Enter your AyaNova 8 SuperUser account login credentials. No other account is permitted to perform this operation. The default SuperUser account login name is "superuser" and password is "l3tm3in" if it has not been changed.
|
||||
- Enter your AyaNova 8 SuperUser account login credentials. No other account is permitted to perform this operation. The default SuperUser account login name is "superuser" and password is "l3tm3in" if it has not been changed. If the AyaNova 8 server has a purchased license the SuperUser password must be changed from the default first (see [Default SuperUser login](ay-start-login.md#default-superuser-login)).
|
||||
- If [two factor authentication](home-tfa.md) is turned on for the AyaNova 8 SuperUser account, turn it off before running the migration and turn it back on afterwards; the V8 Migrate plugin can not log in with two factor authentication and the migration will stop with an error.
|
||||
- A **Migration log** form will popup and show the progress of the Migration operation including any errors or specific action required for your particular data.
|
||||
- Do **NOT** login to the AyaNova 8 server while migration is taking place, it will crash the migration completely.
|
||||
- There is no easy way to estimate how long migration will take which is why several test migrations should be done before the final migration. Time of migration depends on the network latency and speed between the V8 Migrate plugin and the AyaNova 8 server as well as the number and complexity of objects entered into AyaNova and the quantity and size of files that may need to be uploaded.
|
||||
|
||||
@@ -6,7 +6,7 @@ The "desktop" installation is appropriate for a single user of AyaNova on a Linu
|
||||
|
||||
## Linux distributions
|
||||
|
||||
These instructions were compiled and tested on an Ubuntu 22.04 Desktop OS however any Linux x64 distribution should work provided the ASP.NET Core framework can be installed to it.
|
||||
These instructions were originally compiled and tested on an Ubuntu 22.04 Desktop OS however any Linux x64 distribution should work provided the ASP.NET Core 10 runtime can be installed to it.
|
||||
|
||||
You may need to "translate" these instructions into your Linux distribution's package manager and shell equivalent to the Apt package manager and Bash shell script commands shown here.
|
||||
|
||||
@@ -24,13 +24,18 @@ You may need to "translate" these instructions into your Linux distribution's pa
|
||||
|
||||
### 1\. ASP.NET Core runtime
|
||||
|
||||
AyaNova requires the ASP.NET Core runtime version 8.X.x and Microsoft provides a step by step guide to install it properly on each version of Linux.
|
||||
AyaNova requires the ASP.NET Core runtime version 10.x (AyaNova 8.3.0 and newer; earlier versions used 8.x).
|
||||
|
||||
Install the **"ASP.NET CORE RUNTIME" (NOT SDK VERSION)** using this guide for your specific OS version [https://docs.microsoft.com/en-us/dotnet/core/install/linux](https://docs.microsoft.com/en-us/dotnet/core/install/linux)
|
||||
On Ubuntu 24.04 and 26.04 it is in Ubuntu's own package archive:
|
||||
|
||||
There are different procedures for different versions of Linux so be sure to read through the instructions carefully.
|
||||
```bash
|
||||
sudo apt-get update && \
|
||||
sudo apt-get install -y aspnetcore-runtime-10.0
|
||||
```
|
||||
|
||||
There are three different versions of .net core listed, be sure you select the "ASP.NET Core Runtime" version.
|
||||
On Ubuntu 22.04 it is not in Ubuntu's archive; add the Ubuntu .NET backports package repository first (`sudo add-apt-repository ppa:dotnet/backports`) then run the commands above.
|
||||
|
||||
For other Linux distributions Microsoft provides a step by step guide; install the **"ASP.NET CORE RUNTIME" (NOT SDK VERSION)** using this guide for your specific OS version [https://docs.microsoft.com/en-us/dotnet/core/install/linux](https://docs.microsoft.com/en-us/dotnet/core/install/linux)
|
||||
|
||||
Confirm ASP.NET Core runtime is installed and available before proceeding to the next step:
|
||||
|
||||
@@ -38,14 +43,14 @@ Confirm ASP.NET Core runtime is installed and available before proceeding to the
|
||||
dotnet --list-runtimes
|
||||
```
|
||||
|
||||
The AspNetCore runtime should be displayed with version 8.x.x, for example:
|
||||
The AspNetCore runtime should be displayed with version 10.x.x, for example:
|
||||
|
||||
```
|
||||
Microsoft.AspNetCore.App 8.0.3 [/usr/lib/dotnet/shared/Microsoft.AspNetCore.App]
|
||||
Microsoft.NETCore.App 8.0.3 [/usr/lib/dotnet/shared/Microsoft.NETCore.App]
|
||||
Microsoft.AspNetCore.App 10.0.13 [/usr/lib/dotnet/shared/Microsoft.AspNetCore.App]
|
||||
Microsoft.NETCore.App 10.0.13 [/usr/lib/dotnet/shared/Microsoft.NETCore.App]
|
||||
```
|
||||
|
||||
If you do not see '8' as the first major version digit (the rest may vary) go through the installation steps above again.
|
||||
If you do not see '10' as the first number of the version (the rest may vary) go through the installation steps above again.
|
||||
|
||||
### 2\. PostgreSQL server
|
||||
|
||||
@@ -71,21 +76,15 @@ ALTER USER postgres PASSWORD 'YOUR_PASSWORD_HERE';
|
||||
|
||||
If successful psql will display "ALTER ROLE" in response and you can close the psql console by typing `\q` to quit.
|
||||
|
||||
**Warning**: Be careful about using special characters for your Postgres connection string, particularly the password and database name, we've encountered issues with the automatic internal backup failing due to a @ or $ symbol in the password. On some platforms when AyaNova attempts to run the backup utility it may fail if that operating system does not support certain special characters found in the password as they may interfere with the "shell" command called to perform the backup.
|
||||
**Note**: From AyaNova 8.3.0 the automatic internal backup passes the database password to the backup utility directly, without a command shell, so any characters can be used in the PostgreSQL password and database name. Versions older than 8.3.0 could fail to back up if the password contained characters such as `@`, `$`, `:`, `/`, `?`, `#`, `%` or spaces; if you are on an older version, upgrade or avoid those characters.
|
||||
|
||||
### 4\. Create directorys for AyaNova
|
||||
|
||||
AyaNova requires two directorys to be created, one to hold the AyaNova binary and other AyaNova server and web application files and one to hold the AyaNova data. AyaNova will need to write to both these locations so be sure you choose locations with appropriate permissions.
|
||||
AyaNova uses two directories, one to hold the AyaNova binary and other AyaNova server and web application files and one to hold the AyaNova data.
|
||||
|
||||
From the terminal:
|
||||
The data directory does not need to be created: AyaNova creates it the first time it starts. The default setting in AyaNova's configuration is `~/.ayanova-data`. Because AyaNova is started with `sudo` (see below) it runs as the root user, so `~` is root's home directory and the data is kept in `/root/.ayanova-data`. To keep the data somewhere else, set [AYANOVA_DATA_PATH](ops-config-data-path.md) in step 6 to a full path, for example `/opt/ayanova-data`.
|
||||
|
||||
Create the data directory, for desktop linux we recommend your user home directory and AyaNova defaults to '~/.ayanova-data' in it's configuration, however you can use any location that you have write access to.
|
||||
|
||||
```bash
|
||||
sudo mkdir ~/.ayanova-data
|
||||
```
|
||||
|
||||
Create the AyaNova binary files directory. For desktop linux we recommend `/opt/ayanova`.
|
||||
From the terminal, create the AyaNova binary files directory. For desktop linux we recommend `/opt/ayanova`.
|
||||
|
||||
```bash
|
||||
sudo mkdir /opt/ayanova
|
||||
|
||||
@@ -14,7 +14,7 @@ These instructions are targeted towards getting a new Linux server installation
|
||||
|
||||
## Linux distributions
|
||||
|
||||
These instructions were compiled and tested on an Ubuntu 22.04 and 24.04 lts server OS however any Linux x64 distribution should work provided the ASP.NET Core framework can be installed to it.
|
||||
These instructions were compiled and tested on Ubuntu 24.04 and 26.04 LTS server OS however any Linux x64 distribution should work provided the ASP.NET Core 10 runtime can be installed to it.
|
||||
We strongly recommend using an LTS long term support release of Linux in order to minimize business disruption of the non lts versions that need to be upgraded more frequently.
|
||||
|
||||
## Hosted linux
|
||||
@@ -33,7 +33,7 @@ An up to date modern web browser (not Internet Explorer) with Javascript enabled
|
||||
|
||||
## System requirements
|
||||
|
||||
- OS : Linux x64 operating system [supported by ASP.NET Core 8.x](https://docs.microsoft.com/en-us/dotnet/core/install/linux)
|
||||
- OS : Linux x64 operating system [supported by ASP.NET Core 10.x](https://docs.microsoft.com/en-us/dotnet/core/install/linux)
|
||||
- Internet connection : required to install license and to automatically download the Chrome report rendering engine
|
||||
- Disk space: minimum 2GB of free disk space
|
||||
- RAM : 1GB (minimum), 2+ GB (recommended)
|
||||
@@ -61,29 +61,34 @@ We recommend double the size of ram allocated to swap space so if you have 1GB o
|
||||
|
||||
### 2\. Install ASP.NET Core runtime
|
||||
|
||||
AyaNova requires the ASP.NET Core runtime version 8.x and Microsoft provides a step by step guide to install it properly on each version of Linux.
|
||||
AyaNova requires the ASP.NET Core runtime version 10.x (AyaNova 8.3.0 and newer; earlier versions used 8.x).
|
||||
|
||||
Install the **"ASP.NET CORE RUNTIME" (NOT SDK VERSION)** using this guide for your specific OS
|
||||
On Ubuntu 24.04 and 26.04 it is in Ubuntu's own package archive:
|
||||
|
||||
```bash
|
||||
sudo apt-get update && \
|
||||
sudo apt-get install -y aspnetcore-runtime-10.0
|
||||
```
|
||||
|
||||
On Ubuntu 22.04 it is not in Ubuntu's archive; add the Ubuntu .NET backports package repository first (`sudo add-apt-repository ppa:dotnet/backports`) then run the commands above.
|
||||
|
||||
For other Linux distributions Microsoft provides a step by step guide; install the **"ASP.NET CORE RUNTIME" (NOT SDK VERSION)** using this guide for your specific OS
|
||||
[https://docs.microsoft.com/en-us/dotnet/core/install/linux](https://docs.microsoft.com/en-us/dotnet/core/install/linux)
|
||||
|
||||
There are three different versions of .net core listed, be sure you select the "ASP.NET Core Runtime" version.
|
||||
|
||||
There are different procedures for different versions of Linux and there may be a step **before** the actual installation to add the microsoft package repository so be sure to read through the instructions carefully.
|
||||
|
||||
Confirm ASP.NET Core runtime is installed and available before proceeding to the next step:
|
||||
|
||||
```bash
|
||||
dotnet --list-runtimes
|
||||
```
|
||||
|
||||
The AspNetCore runtime should be displayed with version 8.x.x, for example:
|
||||
The AspNetCore runtime should be displayed with version 10.x.x, for example:
|
||||
|
||||
```
|
||||
Microsoft.AspNetCore.App 8.0.3 [/usr/share/dotnet/shared/Microsoft.AspNetCore.App]
|
||||
Microsoft.NETCore.App 8.0.3 [/usr/share/dotnet/shared/Microsoft.NETCore.App]
|
||||
Microsoft.AspNetCore.App 10.0.13 [/usr/lib/dotnet/shared/Microsoft.AspNetCore.App]
|
||||
Microsoft.NETCore.App 10.0.13 [/usr/lib/dotnet/shared/Microsoft.NETCore.App]
|
||||
```
|
||||
|
||||
If you do not see an '8' as the first digit of the version number (the rest may vary) go through the installation steps above again.
|
||||
If you do not see '10' as the first number of the version (the rest may vary) go through the installation steps above again.
|
||||
|
||||
### 3\. Install PostgreSQL server
|
||||
|
||||
@@ -112,7 +117,7 @@ ALTER USER postgres PASSWORD 'YOUR_PASSWORD_HERE';
|
||||
|
||||
If successful psql will display "ALTER ROLE" in response and you can close the psql console by typing `\q` to quit.
|
||||
|
||||
**Warning**: Be careful about using special characters for your Postgres connection string, particularly the password and database name, we've encountered issues with the automatic internal backup failing due to a @ or $ symbol in the password. On some platforms when AyaNova attempts to run the backup utility it may fail if that operating system does not support certain special characters found in the password as they may interfere with the "shell" command called to perform the backup.
|
||||
**Note**: From AyaNova 8.3.0 the automatic internal backup passes the database password to the backup utility directly, without a command shell, so any characters can be used in the PostgreSQL password and database name. Versions older than 8.3.0 could fail to back up if the password contained characters such as `@`, `$`, `:`, `/`, `?`, `#`, `%` or spaces; if you are on an older version, upgrade or avoid those characters.
|
||||
|
||||
### 5\. Install Chrome browser support libraries
|
||||
|
||||
@@ -120,27 +125,27 @@ AyaNova uses a "headless" Chrome browser to render report templates. Server vers
|
||||
|
||||
These examples below are tested with Ubuntu LTS releases; your distro may require different files or the requirements may change for Chrome over time. If in doubt, run a report and if you see an error check the [server log](ops-log.md) and if a library is missing causing Chrome to fail the error in the log will show the missing library file that will need to be installed if that is the issue.
|
||||
|
||||
#### Ubuntu 24.04 and 26.04 Chrome support libraries
|
||||
|
||||
```bash
|
||||
sudo apt-get update && \
|
||||
sudo apt-get install -y libatk1.0-0t64 libc6 libcairo2 libdbus-1-3 libexpat1 libfontconfig1 libgcc-s1 libgdk-pixbuf-2.0-0 libglib2.0-0t64 libgtk-3-0t64 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libnss3 lsb-release libgbm-dev xdg-utils && \
|
||||
sudo apt-get install -y libatk-bridge2.0-0t64 libcups2t64 libxcomposite1 libxdamage1 libpangocairo-1.0-0 libxfixes3 libxrandr2 libgbm1 libcairo2 libpango-1.0-0 libasound2t64 wget
|
||||
```
|
||||
|
||||
#### Ubuntu 22.04 Chrome support libraries
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y libatk1.0-0 libatk-bridge2.0-0 libcups2 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libxkbcommon0 libpango-1.0-0 libcairo2 libasound2 fonts-liberation
|
||||
```
|
||||
|
||||
#### Ubuntu 24.04 Chrome support libraries
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y libatk1.0-0t64 libc6 libcairo2 libdbus-1-3 libexpat1 libfontconfig1 libgcc-s1 libgdk-pixbuf2.0-0 libglib2.0-0t64 libgtk-3-0t64 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libnss3 lsb-release libgbm-dev xdg-utils \
|
||||
sudo apt-get update \
|
||||
sudo apt-get install -y libatk-bridge2.0-0t64 libcups2t64 libxcomposite1 libxdamage1 libpangocairo-1.0-0 libxfixes3 libxrandr2 libgbm1 libcairo2 libpango-1.0-0 libasound2t64 wget
|
||||
```
|
||||
|
||||
#### Fonts / layout
|
||||
|
||||
All stock reports included with AyaNova are designed to layout correctly as long as the specified fonts are available. If you find the layouts are broken on the stock reports or don't align properly be sure that the `fonts-liberation` package is installed to your server.
|
||||
|
||||
### 6\. Create directories for AyaNova
|
||||
|
||||
AyaNova requires several directories to be created: the AyaNova binary and other server and web application files, AyaNova data and the folders to contain a local copy of the Chrome browser for report rendering.
|
||||
AyaNova requires two directories to be created: one for the AyaNova binary and other server and web application files and one for AyaNova data (which also holds the downloaded Chrome browser for report rendering).
|
||||
|
||||
Create the AyaNova binary files directory. We recommend `/var/ayanova`:
|
||||
|
||||
@@ -154,15 +159,7 @@ Create the data directory. AyaNova will automatically create subfolders under th
|
||||
sudo mkdir /var/ayanova/data
|
||||
```
|
||||
|
||||
Create the Chrome binary directories:
|
||||
|
||||
```bash
|
||||
sudo mkdir /var/ayanova/Chrome
|
||||
```
|
||||
|
||||
```bash
|
||||
sudo mkdir /var/ayanova/ChromeHeadlessShell
|
||||
```
|
||||
The Chrome report rendering engine is downloaded automatically into a `report-browser` folder under this data directory the first time a report is rendered, so the data directory must not be on a `noexec` mount.
|
||||
|
||||
### 7\. Download and unzip the AyaNova binaries archive
|
||||
|
||||
@@ -200,16 +197,6 @@ The [AYANOVA_DB_CONNECTION](ops-config-db.md) property must be edited and where
|
||||
|
||||
The [AYANOVA_USE_URLS](ops-config-use-urls.md) property specifies which TCP / IP Port that AyaNova server should listen on for connections from web browsers. The default value is port 7575 which should be free in most cases, however if you need to use an alternate port due to conflict it must be set here. We recommend using the default 7575 to avoid current or future confusion or conflict with other servers that may be installed or present elsewhere on the network.
|
||||
|
||||
Optional but recommended: Add a 32 character [AYANOVA_JWT_SECRET](ops-config-jwt-secret.md) property to the configuration if you want to ensure that logged in User's session can survive a restart of the AyaNova server. If this is not set AyaNova will invalidate all login tokens any time the server restarts by generating a new random JWT secret on boot.
|
||||
|
||||
```json
|
||||
{
|
||||
...existing properties...
|
||||
"AYANOVA_LOG_LEVEL": "Info",
|
||||
"AYANOVA_JWT_SECRET": "1111111MyRandom32CharacterSecret"
|
||||
}
|
||||
```
|
||||
|
||||
Edit the config.json file in your favorite editor, here we'll use Nano:
|
||||
|
||||
```bash
|
||||
@@ -228,28 +215,22 @@ Give www-data ownership of the ayanova directory:
|
||||
sudo chown -vR :www-data /var/ayanova
|
||||
```
|
||||
|
||||
Set permissions to read / write for the AyaNova data directory:
|
||||
Make the AyaNova program files read only for everyone but root, so the account AyaNova runs as can not change them:
|
||||
|
||||
```bash
|
||||
sudo chmod -R go-w /var/ayanova
|
||||
```
|
||||
|
||||
Set permissions to read / write for the AyaNova data directory, the only place AyaNova needs to write to:
|
||||
|
||||
```bash
|
||||
sudo chmod -R g+rw /var/ayanova/data
|
||||
```
|
||||
|
||||
Set permissions to read / write / execute for the .Chrome directory:
|
||||
The config.json file holds the database password, make it readable only by root and AyaNova:
|
||||
|
||||
```bash
|
||||
chmod -R g+rwx /var/ayanova/Chrome
|
||||
```
|
||||
|
||||
Set permissions to read / write / execute for the .ChromeHeadlessShell directory:
|
||||
|
||||
```bash
|
||||
chmod -R g+rwx /var/ayanova/ChromeHeadlessShell
|
||||
```
|
||||
|
||||
Set permissions for the AyaNova folder
|
||||
|
||||
```bash
|
||||
chmod g+rw /var/ayanova/
|
||||
sudo chmod 640 /var/ayanova/config.json
|
||||
```
|
||||
|
||||
### 10\. Create and install a service to run AyaNova automatically
|
||||
@@ -279,6 +260,12 @@ SyslogIdentifier=ayanova-server
|
||||
User=www-data
|
||||
Environment=ASPNETCORE_ENVIRONMENT=Production
|
||||
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false
|
||||
# Security: AyaNova can write only to its data directory
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=/var/ayanova/data
|
||||
PrivateTmp=true
|
||||
ProtectHome=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -286,6 +273,8 @@ WantedBy=multi-user.target
|
||||
|
||||
Make the changes and save with `ctr-x` and type `y` to answer "Save modified buffer?" prompt.
|
||||
|
||||
The lines under `# Security` limit what the AyaNova service can do on the server: it can not gain extra privileges, sees the rest of the system as read only and can write only to its data directory. AyaNova keeps all of its files (logs, attachments, backups, temporary files and the report rendering browser) in the data directory by default so nothing else is needed. If you have set a [log](ops-log.md#log-path), [attachments](ops-config-folder-user-files.md), [backup](ops-config-folder-backup-files.md) or [temporary files](ops-config-folder-temporary-files.md) path outside of the data directory, add each of those folders to the `ReadWritePaths` line separated by spaces, otherwise AyaNova will not be able to write to them.
|
||||
|
||||
Enable the service:
|
||||
|
||||
```bash
|
||||
@@ -348,6 +337,8 @@ These instructions will assume use of a free SSL certificate from [LetsEncrypt](
|
||||
|
||||
We will provide instructions for using the free and open source [NGINX](https://nginx.org/) as a reverse proxy server, if you already have another server capable of being a reverse proxy (such as Apache) you will need to adapt these instructions to match your situation.
|
||||
|
||||
These instructions run the reverse proxy on the same server as AyaNova. If your reverse proxy runs on a different computer, see [trusted reverse proxies](ops-config-trusted-proxies.md).
|
||||
|
||||
#### A\. Install NGINX
|
||||
|
||||
Install NGINX to be the reverse proxy server:
|
||||
@@ -637,7 +628,33 @@ Certbot will also install a systemd timer event which will auto-renew the SSL ce
|
||||
|
||||
Confirm that you can now navigate to AyaNova web app login page through encrypted HTTPS via the domain name configured earlier. i.e. `https://ayanova.yourdomain.com` or `https://yourdomain.com` if not using a subdomain.
|
||||
|
||||
You should now see the login (or license agreement) page through an encrypted SSL connection and AyaNova is ready for use.
|
||||
You should now see the login (or license agreement) page through an encrypted SSL connection.
|
||||
|
||||
#### F\. Accept connections only through NGINX
|
||||
|
||||
AyaNova still listens on port 7575 on all of the server's network addresses, where it can be reached directly over unencrypted HTTP, bypassing NGINX and its encryption. Now that NGINX is working, set AyaNova to accept connections only from this server itself so all users connect through NGINX.
|
||||
|
||||
Edit the [AYANOVA_USE_URLS](ops-config-use-urls.md) setting in config.json:
|
||||
|
||||
```bash
|
||||
sudo nano /var/ayanova/config.json
|
||||
```
|
||||
|
||||
and change `http://*:7575` to `http://127.0.0.1:7575` so the line reads:
|
||||
|
||||
```json
|
||||
"AYANOVA_USE_URLS": "http://127.0.0.1:7575",
|
||||
```
|
||||
|
||||
Save, then restart AyaNova:
|
||||
|
||||
```bash
|
||||
sudo systemctl restart ayanova.service
|
||||
```
|
||||
|
||||
Confirm again that the login page opens through `https://` as in the previous step. AyaNova is now ready for use.
|
||||
|
||||
Only make this change if all users connect through NGINX: users connecting directly to port 7575 on the local network will no longer be able to connect and must use the `https://` address instead.
|
||||
|
||||
## Getting started and using AyaNova
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ Select the OK button and Windows will enable IIS. Reboot is not required at this
|
||||
|
||||
AyaNova requires access to a PostgreSQL database server version 12 or newer. It should be up and running **before** AyaNova is installed. If you do not have a PostgreSQL server already then it must be installed to a location visible to the AyaNova server (same computer or a local computer on the same network). PostgreSQL is open source and free to use.
|
||||
|
||||
If you need to install PostgreSQL follow this installation guide [https://www.postgresqltutorial.com/install-postgresql/](https://www.postgresqltutorial.com/install-postgresql/)
|
||||
If you need to install PostgreSQL, download the Windows installer from the official PostgreSQL download page [https://www.postgresql.org/download/windows/](https://www.postgresql.org/download/windows/) and run it with the default options. The examples below assume PostgreSQL 18, the current version.
|
||||
|
||||
Note: we have seen cases during testing where the PostgreSQL installer will show error messages related to Visual C++ runtime file installation on the latest Windows server platforms however this appears to be cosmetic only and doesn't affect operation of PostgreSQL.
|
||||
|
||||
@@ -101,7 +101,7 @@ Note: we have seen cases during testing where the PostgreSQL installer will show
|
||||
|
||||
If you are installing PostgreSQL you will be prompted to provide a password to access it. Be sure to make a note of this password as AyaNova will need it to be specified later in the `AYANOVA_DB_CONNECTION` [configuration setting](ops-config-environment-variables.md) after installing the AyaNova server.
|
||||
|
||||
**Warning**: Be careful about using special characters for your Postgres connection string, particularly the password and database name, we've encountered issues with the automatic internal backup failing due to a @ or $ symbol in the password. On some platforms when AyaNova attempts to run the backup utility it may fail if that operating system does not support certain special characters found in the password as they may interfere with the "shell" command called to perform the backup.
|
||||
**Note**: From AyaNova 8.3.0 the automatic internal backup passes the database password to the backup utility directly, without a command shell, so any characters can be used in the PostgreSQL password and database name. Versions older than 8.3.0 could fail to back up if the password contained characters such as `@`, `$`, `:`, `/`, `?`, `#`, `%` or spaces; if you are on an older version, upgrade or avoid those characters.
|
||||
|
||||
### 3\. ASP.NET Core Runtime
|
||||
|
||||
@@ -111,11 +111,11 @@ If IIS is not already enabled and running do not proceed until it is.
|
||||
|
||||
The ASP.NET Core runtime will see IIS and install appropriate components but only if IIS is present during the ASP.NET Core runtime installation.
|
||||
|
||||
AyaNova is built upon the ASP.NET Core runtime version 8.x.x and it should be present **before** AyaNova is installed.
|
||||
AyaNova is built upon the ASP.NET Core runtime version 10.x.x (AyaNova 8.3.0 and newer) and it should be present **before** AyaNova is installed.
|
||||
|
||||
**Warning** Microsoft provides several different links to "asp.net core runtime" downloads; the one required by AyaNova is the link labelled "Hosting bundle" and it is in the runtimes section under Windows.
|
||||
|
||||
Download the ASP.NET Core **Hosting bundle** runtime here [https://dotnet.microsoft.com/en-us/download/dotnet/8.0](https://dotnet.microsoft.com/en-us/download/dotnet/8.0).
|
||||
Download the ASP.NET Core **Hosting bundle** runtime here [https://dotnet.microsoft.com/en-us/download/dotnet/10.0](https://dotnet.microsoft.com/en-us/download/dotnet/10.0).
|
||||
|
||||
Run the installer and take the defaults.
|
||||
|
||||
@@ -125,17 +125,19 @@ Confirm your ASP.NET Core runtime is installed and running by opening a DOS comm
|
||||
dotnet --list-runtimes
|
||||
```
|
||||
|
||||
The AspNetCore runtime should be displayed with version 8.x.x, for example:
|
||||
The AspNetCore runtime should be displayed with version 10.x.x, for example:
|
||||
|
||||
```
|
||||
Microsoft.AspNetCore.App 8.0.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
|
||||
Microsoft.NETCore.App 8.0.5 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
|
||||
Microsoft.AspNetCore.App 10.0.13 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
|
||||
Microsoft.NETCore.App 10.0.13 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
|
||||
```
|
||||
|
||||
(your version may be newer)
|
||||
|
||||
### 4\. AyaNova server installation
|
||||
|
||||
**Note**: if another type of AyaNova 8 installation is already on this computer, uninstall it first (AyaNova 7 can stay); see [only one type of AyaNova 8 installation per computer](ops-install-windows.md).
|
||||
|
||||
Once you have ensured the three requirements above are installed and available, download and run the AyaNova network installer: [https://www.ayanova.com/download/ayanova-windows-x64-lan-setup.exe](https://www.ayanova.com/download/ayanova-windows-x64-lan-setup.exe).
|
||||
|
||||
#### Smart screen warning
|
||||
@@ -203,7 +205,7 @@ The default installed `config.json` file will initially have the following:
|
||||
"AYANOVA_USE_URLS": "http://*:7575",
|
||||
"AYANOVA_DB_CONNECTION": "Server=localhost;Username=postgres;Password=YOUR_PASSWORD_HERE;Database=AyaNova;",
|
||||
"AYANOVA_DEFAULT_TRANSLATION": "en",
|
||||
"AYANOVA_BACKUP_PG_DUMP_PATH": "C:\\Program Files\\PostgreSQL\\16\\bin",
|
||||
"AYANOVA_BACKUP_PG_DUMP_PATH": "C:\\Program Files\\PostgreSQL\\18\\bin",
|
||||
"AYANOVA_DATA_PATH": "%ProgramData%\\ayanova",
|
||||
"AYANOVA_LOG_LEVEL": "Info"
|
||||
}
|
||||
@@ -227,23 +229,9 @@ EDIT ONLY IF NEEDED:
|
||||
|
||||
The [AYANOVA_BACKUP_PG_DUMP_PATH](ops-config-backup-pgdump-path.md) property specifies the path to the PostgreSQL database backup utility `pg_dump` if AyaNova server will be doing the [automatic daily backups](ops-form-backup.md).
|
||||
|
||||
A. if AyaNova server will be doing the automatic daily backups using AyaNova's Backup feature, confirm this path stated matches the path to YOUR PostgreSQL database backup utility pg_dump
|
||||
A. if AyaNova server will be doing the automatic daily backups using AyaNova's Backup feature, confirm this path stated matches the path to YOUR PostgreSQL database backup utility pg_dump. The `18` in the path is the PostgreSQL version number: if you installed a different version, change it to match (for example `C:\\Program Files\\PostgreSQL\\17\\bin`)
|
||||
B. otherwise, If you already have a backup solution in place for your PostgreSQL databases AND WILL NOT BE USING AYANOVA'S Backup feature, remove this
|
||||
|
||||
OPTIONAL EDIT:
|
||||
|
||||
Modify the 32 character [AYANOVA_JWT_SECRET](ops-config-jwt-secret.md) property of the configuration which ensures that the logged in User's session can survive a restart of the AyaNova server. If this is not set AyaNova will invalidate all login tokens any time the server restarts by generating a new random JWT secret on boot which under IIS happens frequently.
|
||||
|
||||
Change this from the default of "11111YourRandom32CharacterSecret" to be unique to your site (must be 32 characters).
|
||||
|
||||
```json
|
||||
{
|
||||
...existing properties...
|
||||
"AYANOVA_LOG_LEVEL": "Info",
|
||||
"AYANOVA_JWT_SECRET": "11111YourRandom32CharacterSecret"
|
||||
}
|
||||
```
|
||||
|
||||
After editing, saving and exiting out of NotePad, open the config.json file AGAIN to confirm that the changes were saved correctly and that the file is still specifically labeled with the name and extension config.json
|
||||
|
||||
### 6\. Boot and test the server
|
||||
@@ -402,15 +390,13 @@ Let's Encrypt provides free SSL certificates and lists [several Windows clients]
|
||||
|
||||
The very first time a report is rendered in AyaNova there will be a delay while the Chromium browser report rendering components are installed.
|
||||
|
||||
#### Report rendering error
|
||||
#### Report rendering under IIS
|
||||
|
||||
In some cases there may be a futher setting required for report rendering.
|
||||
Reports, including reports attached to customer notifications, render under IIS with any site bindings, HTTPS included, and no further setting is needed. When AyaNova runs inside IIS, the requests a report makes to the AyaNova server are sent by AyaNova itself to the same address and site name that users connect to.
|
||||
|
||||
If you render a report and see this error:
|
||||
Earlier versions of AyaNova needed the [AYANOVA_REPORT_RENDER_API_URL_OVERRIDE](ops-config-report-rendering-api-url-override.md) setting under some IIS configurations. If it is set from an earlier version it does no harm and can be removed.
|
||||
|
||||
`JobFailed: PuppeteerSharp -> Evaluation failed: TypeError: Failed to fetch at ayPostToAPI`
|
||||
|
||||
You will need to set the [AYANOVA_REPORT_RENDER_API_URL_OVERRIDE](ops-config-report-rendering-api-url-override.md) setting in the [AyaNova server configuration](#5-ayanova-server-configuration) to your AyaNova server's external URL address to enable report rendering for your configuration.
|
||||
If a report fails with an error that mentions "AyaNova API request ... failed", the error text says why. For example, the site's SSL certificate must be valid for the site name users connect to.
|
||||
|
||||
#### Getting started and using AyaNova
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ An up to date modern web browser (not Internet Explorer) with Javascript enabled
|
||||
|
||||
- Program files : By default (and recommended) AyaNova server program files will be installed to `C:\Program Files\ayanova` folder
|
||||
- Data files: All data aside from attachment files is stored in the PostgreSQL database. By default AyaNova will store attachment files, logs and backup files in the %ProgramData% folder under `ayanova` which on most computers will be located at `C:\ProgramData\ayanova`. Each folder location can be changed via [configuration](ops-config-environment-variables.md).
|
||||
- Folder permissions: only administrators can change files in the program folder, so `config.json` must be edited with Notepad run as administrator (see below). Every user of the computer can change files in the data folder, because the AyaNova server runs as whichever user starts it. If the computer is shared with people who should not have access to AyaNova's data, limit the data folder to the Windows account that runs the AyaNova server. The `config.json` file contains the database password and can be read by every user of the computer; on a shared computer limit it to Administrators and the Windows account that runs the AyaNova server.
|
||||
|
||||
## Installation procedure
|
||||
|
||||
@@ -35,7 +36,7 @@ AyaNova requires access to a PostgreSQL database server version 12 or newer. It
|
||||
|
||||
PostgreSQL is open source and free to use.
|
||||
|
||||
If you need to install PostgreSQL follow this installation guide [https://www.postgresqltutorial.com/install-postgresql/](https://www.postgresqltutorial.com/install-postgresql/)
|
||||
If you need to install PostgreSQL, download the Windows installer from the official PostgreSQL download page [https://www.postgresql.org/download/windows/](https://www.postgresql.org/download/windows/) and run it with the default options. The examples below assume PostgreSQL 18, the current version.
|
||||
|
||||
Note: we have seen cases during testing where the PostgreSQL installer will show error messages related to Visual C++ runtime file installation on the latest Windows server platforms however this appears to be cosmetic only and doesn't affect operation of PostgreSQL.
|
||||
|
||||
@@ -43,15 +44,15 @@ Note: we have seen cases during testing where the PostgreSQL installer will show
|
||||
|
||||
If you are installing PostgreSQL you will be prompted to provide a password to access it. Be sure to make a note of this password as AyaNova will need it to be specified later in the `AYANOVA_DB_CONNECTION` [configuration setting](ops-config-environment-variables.md) after installing the AyaNova server.
|
||||
|
||||
**Warning**: Be careful about using special characters for your Postgres connection string, particularly the password and database name, we've encountered issues with the automatic internal backup failing due to a @ or $ symbol in the password. On some platforms when AyaNova attempts to run the backup utility it may fail if that operating system does not support certain special characters found in the password as they may interfere with the "shell" command called to perform the backup.
|
||||
**Note**: From AyaNova 8.3.0 the automatic internal backup passes the database password to the backup utility directly, without a command shell, so any characters can be used in the PostgreSQL password and database name. Versions older than 8.3.0 could fail to back up if the password contained characters such as `@`, `$`, `:`, `/`, `?`, `#`, `%` or spaces; if you are on an older version, upgrade or avoid those characters.
|
||||
|
||||
### 2\. Install ASP.NET Core Runtime
|
||||
|
||||
AyaNova is built upon the ASP.NET Core runtime version 8.x.x and it should be present **before** AyaNova is installed.
|
||||
AyaNova is built upon the ASP.NET Core runtime version 10.x.x (AyaNova 8.3.0 and newer) and it should be present **before** AyaNova is installed.
|
||||
|
||||
**Warning** Microsoft provides several different links to "asp.net core runtime" downloads; the one required by AyaNova is the link labelled "Hosting bundle".
|
||||
|
||||
Download the ASP.NET Core **Hosting bundle** runtime here [https://dotnet.microsoft.com/en-us/download/dotnet/8.0](https://dotnet.microsoft.com/en-us/download/dotnet/8.0).
|
||||
Download the ASP.NET Core **Hosting bundle** runtime here [https://dotnet.microsoft.com/en-us/download/dotnet/10.0](https://dotnet.microsoft.com/en-us/download/dotnet/10.0).
|
||||
|
||||
Run the installer and take the defaults.
|
||||
|
||||
@@ -61,17 +62,19 @@ Confirm your ASP.NET Core runtime is installed and running by opening a DOS comm
|
||||
dotnet --list-runtimes
|
||||
```
|
||||
|
||||
The AspNetCore runtime should be displayed with version 8.x.x, for example:
|
||||
The AspNetCore runtime should be displayed with version 10.x.x, for example:
|
||||
|
||||
```
|
||||
Microsoft.AspNetCore.App 8.0.5 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
|
||||
Microsoft.NETCore.App 8.0.5 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
|
||||
Microsoft.AspNetCore.App 10.0.13 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
|
||||
Microsoft.NETCore.App 10.0.13 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
|
||||
```
|
||||
|
||||
(your version may be newer)
|
||||
|
||||
### 3\. AyaNova server installation
|
||||
|
||||
**Note**: if another type of AyaNova 8 installation is already on this computer, uninstall it first (AyaNova 7 can stay); see [only one type of AyaNova 8 installation per computer](ops-install-windows.md).
|
||||
|
||||
Once you have ensured the two requirements above are installed and available; download and run the AyaNova network installer: [https://www.ayanova.com/download/ayanova-windows-x64-lan-setup.exe](https://www.ayanova.com/download/ayanova-windows-x64-lan-setup.exe).
|
||||
|
||||
#### Smart screen warning
|
||||
@@ -135,7 +138,7 @@ The default installed `config.json` file will initially have the following:
|
||||
"AYANOVA_USE_URLS": "http://*:7575",
|
||||
"AYANOVA_DB_CONNECTION": "Server=localhost;Username=postgres;Password=YOUR_PASSWORD_HERE;Database=AyaNova;",
|
||||
"AYANOVA_DEFAULT_TRANSLATION": "en",
|
||||
"AYANOVA_BACKUP_PG_DUMP_PATH": "C:\\Program Files\\PostgreSQL\\16\\bin",
|
||||
"AYANOVA_BACKUP_PG_DUMP_PATH": "C:\\Program Files\\PostgreSQL\\18\\bin",
|
||||
"AYANOVA_DATA_PATH": "%ProgramData%\\ayanova",
|
||||
"AYANOVA_LOG_LEVEL": "Info"
|
||||
}
|
||||
@@ -159,23 +162,9 @@ EDIT ONLY IF NEEDED:
|
||||
|
||||
The [AYANOVA_BACKUP_PG_DUMP_PATH](ops-config-backup-pgdump-path.md) property specifies the path to the PostgreSQL database backup utility `pg_dump` if AyaNova server will be doing the [automatic daily backups](ops-form-backup.md).
|
||||
|
||||
A. if AyaNova server will be doing the automatic daily backups using AyaNova's Backup feature, confirm this path stated matches the path to YOUR PostgreSQL database backup utility pg_dump
|
||||
A. if AyaNova server will be doing the automatic daily backups using AyaNova's Backup feature, confirm this path stated matches the path to YOUR PostgreSQL database backup utility pg_dump. The `18` in the path is the PostgreSQL version number: if you installed a different version, change it to match (for example `C:\\Program Files\\PostgreSQL\\17\\bin`)
|
||||
B. otherwise, If you already have a backup solution in place for your PostgreSQL databases AND WILL NOT BE USING AYANOVA'S Backup feature, remove this
|
||||
|
||||
OPTIONAL EDIT:
|
||||
|
||||
Modify the 32 character [AYANOVA_JWT_SECRET](ops-config-jwt-secret.md) property of the configuration which ensures that the logged in User's session can survive a restart of the AyaNova server. If this is not set AyaNova will invalidate all login tokens any time the server restarts by generating a new random JWT secret on boot.
|
||||
|
||||
Change this from the default of "11111YourRandom32CharacterSecret" to be unique to your site (must be 32 characters).
|
||||
|
||||
```json
|
||||
{
|
||||
...existing properties...
|
||||
"AYANOVA_LOG_LEVEL": "Info",
|
||||
"AYANOVA_JWT_SECRET": "11111YourRandom32CharacterSecret"
|
||||
}
|
||||
```
|
||||
|
||||
After editing, saving and exiting out of NotePad, open the config.json file AGAIN to confirm that the changes were saved correctly and that the file is still specifically labeled with the name and extension config.json
|
||||
|
||||
### 5\. Boot and test the server
|
||||
|
||||
@@ -32,9 +32,12 @@ The single user installer will use TCP/IP ports 6432 for the PostgreSQL server a
|
||||
|
||||
- Program files : By default (and recommended) AyaNova server program files will be installed to `C:\Program Files\ayanova` folder
|
||||
- Data files: AyaNova will store all data in the %ProgramData% folder under `ayanova` which on most computers will be located at `C:\ProgramData\ayanova`. This includes all logs, backups and User attachment files.
|
||||
- Folder permissions: only administrators can change files in the program folder. Every user of the computer can change files in the data folder, because AyaNova and its built in database server run as whichever user starts them. If the computer is shared with people who should not have access to AyaNova's data, limit the data folder to the Windows account that runs AyaNova. The `config.json` file in the program folder contains the database password and can be read by every user of the computer; on a shared computer limit it to Administrators and the Windows account that runs AyaNova.
|
||||
|
||||
## Installation procedure
|
||||
|
||||
**Note**: if another type of AyaNova 8 installation is already on this computer, uninstall it first (AyaNova 7 can stay); see [only one type of AyaNova 8 installation per computer](ops-install-windows.md).
|
||||
|
||||
1\. Download and run the standalone installer from our web site
|
||||
|
||||
[https://www.ayanova.com/download/ayanova-windows-x64-single-setup.exe](https://www.ayanova.com/download/ayanova-windows-x64-single-setup.exe)
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Choose how AyaNova will be used for specific Windows installation instructions. Note that it's possible to switch to an alternative configuration via backup and restore if needs change later however if there is a possibility that AyaNova will be accessed from the internet it's recommended to install as an IIS hosted application.
|
||||
|
||||
**Only one type of AyaNova 8 installation per computer:** the single user, local network and IIS installations use the same program folder and network port, and the AyaNova 8 installers treat them as the same program. This does not apply to AyaNova 7, which is a separate program and can stay installed on the same computer. To change an existing installation to another type on the same computer, first [make a backup](ops-form-backup.md) and keep it somewhere safe, uninstall AyaNova from Windows Settings -> Apps, then follow the installation guide for the new type and restore the backup.
|
||||
|
||||
## Single user installation
|
||||
|
||||
This type of install is appropriate for using AyaNova locally on a single personal computer only. This type of installation is _not_ appropriate for sharing with multiple users.
|
||||
|
||||
@@ -46,7 +46,7 @@ Typically this is an email address, for example `noreply@example.com` but some m
|
||||
|
||||
#### SMTP server password
|
||||
|
||||
The password the SMTP server requires to send email using the SMTP server account address. Note that this password is visible in this form to system operators and the read only roles listed above.
|
||||
The password the SMTP server requires to send email using the SMTP server account address. Once saved, the password is never shown in this form: it shows `********` when a password is set. To change it, type the new password and save. Leaving `********` in place keeps the saved password.
|
||||
|
||||
We recommend a dedicated email account used only for sending notifications.
|
||||
|
||||
@@ -60,6 +60,8 @@ The choices are:
|
||||
- SSL \ TLS recommended and most secure. Implicit forced security.
|
||||
- StartTLS not recommended, negotiated security, may or may not be secure depending on the capabilities of the mail server.
|
||||
|
||||
AyaNova checks the mail server's security certificate when it connects. See [SMTP certificate check](ops-config-smtp-accept-any-certificate.md) if the test message reports that the certificate could not be validated.
|
||||
|
||||
#### SMTP server port
|
||||
|
||||
This is the port that must match the SMTP server's mail port. Most modern mail servers use port `587` which is the current recommended standard port for encrypted mail transmission.
|
||||
|
||||
@@ -139,6 +139,16 @@ pg_restore -U postgres -c -d AyaNova ~/downloads/db-20211212195504569.backup
|
||||
|
||||
The restore will complete normally without any message if successful.
|
||||
|
||||
#### If AyaNova connects to PostgreSQL with its own user
|
||||
|
||||
The examples above are for the default setup, where AyaNova connects to PostgreSQL as the `postgres` user (the `Username` in [AYANOVA_DB_CONNECTION](ops-config-db.md)). If you have set AyaNova up to connect as a different, dedicated PostgreSQL user, the restored tables must belong to that user: add `--no-owner --no-privileges --role=` followed by that user name to the restore command. For example, for a PostgreSQL user named `ayanova`:
|
||||
|
||||
```bash
|
||||
pg_restore -U postgres -c --no-owner --no-privileges --role=ayanova -d AyaNova ~/downloads/db-20211212195504569.backup
|
||||
```
|
||||
|
||||
Without these options the restored tables belong to `postgres` and AyaNova will refuse to start with a database integrity error.
|
||||
|
||||
### Linux server only - set ownershipt and permissions
|
||||
|
||||
Before restarting the AyaNova server on Linux it will first be necessary to reset the ownership and permissions for the files and folders or AyaNova will not be able to write it's log, access attachments or generate reports properly.
|
||||
|
||||
@@ -22,23 +22,19 @@ Alternatively you can hard shut down AyaNova using the ctrl-c key combination in
|
||||
|
||||
### 2.5\. Pre-upgrade step
|
||||
|
||||
**Only if** your current AyaNova version is older than 8.2.0 there is an extra step before the normal upgrade process.
|
||||
**Only if** your current AyaNova version is older than 8.3.0 there is an extra step before the normal upgrade process.
|
||||
|
||||
8.2.0 switched to .NET 8 LTS from the prior used .NET 6 LTS and also switched to using Chrome instead of Chromium for the report rendering headless browser so this script accomodates these changes.
|
||||
8.3.0 switched to .NET 10 LTS from the prior used .NET 8 LTS (8.2.0 to 8.2.4), so the .NET 10 runtime must be installed first.
|
||||
|
||||
From a terminal window run the following commands:
|
||||
|
||||
```bash
|
||||
sudo apt-get update && apt-get upgrade && \
|
||||
sudo apt-get install -y aspnetcore-runtime-8.0 && \
|
||||
sudo mkdir /var/ayanova/Chrome && \
|
||||
sudo mkdir /var/ayanova/ChromeHeadlessShell && \
|
||||
sudo chown -vR :www-data /var/ayanova && \
|
||||
sudo chmod -R g+rwx /var/ayanova/Chrome && \
|
||||
sudo chmod -R g+rwx /var/ayanova/ChromeHeadlessShell && \
|
||||
sudo chmod g+rw /var/ayanova/
|
||||
sudo apt-get update && sudo apt-get upgrade && \
|
||||
sudo apt-get install -y aspnetcore-runtime-10.0
|
||||
```
|
||||
|
||||
On Ubuntu 24.04 and 26.04 the .NET 10 runtime comes from Ubuntu's own package archive. On Ubuntu 22.04 first add the Ubuntu .NET backports package repository with `sudo add-apt-repository ppa:dotnet/backports`.
|
||||
|
||||
If there is an issue or error break apart the commands and execute each one individually to find the error.
|
||||
|
||||
After this step has completed you can continue with the normal upgrade process in the next step.
|
||||
@@ -47,13 +43,14 @@ After this step has completed you can continue with the normal upgrade process i
|
||||
|
||||
#### upgrade script
|
||||
|
||||
The following shell script will automatically remove the old AyaNova files, download and unzip the latest AyaNova files then start the server.
|
||||
The following shell script will automatically remove the old AyaNova files, download and unzip the latest AyaNova files then start the server. It first checks that the .NET 10 runtime is installed and stops without changing anything if it is not (see the pre-upgrade step above). If the download fails, it also stops before any files are removed.
|
||||
|
||||
From a terminal window run the following commands:
|
||||
|
||||
```bash
|
||||
cd /opt/ayanova && \
|
||||
sudo curl -O https://www.ayanova.com/download/ayanova-linux-x64-desktop.zip && \
|
||||
{ dotnet --list-runtimes | grep -q '^Microsoft.AspNetCore.App 10\.' || { echo "The .NET 10 runtime is not installed: do the pre-upgrade step first. Nothing was changed."; false; }; } && \
|
||||
sudo curl -fO https://www.ayanova.com/download/ayanova-linux-x64-desktop.zip && \
|
||||
sudo rm *.dll && \
|
||||
sudo rm -r ./resource && \
|
||||
sudo rm -r ./wwwroot && \
|
||||
@@ -64,6 +61,8 @@ sudo ./AyaNova
|
||||
|
||||
If there is an issue or error break apart the commands and execute each one individually to find the error.
|
||||
|
||||
From 8.3.0 the Chrome report rendering engine is downloaded into a `report-browser` folder under the data directory instead of the program folder. After upgrading from an earlier version any old `Chrome` and `ChromeHeadlessShell` folders in the AyaNova program folder are no longer used and can be removed.
|
||||
|
||||
AyaNova should restart on it's own and perform any database schema updates if required automatically.
|
||||
|
||||
Open the login form in your browser and after a short delay you should see the new version number at the bottom of the login form.
|
||||
|
||||
@@ -16,39 +16,58 @@ If you do not have a fresh backup there will be no way to recover from potential
|
||||
|
||||
### 1.5\. Pre-upgrade step
|
||||
|
||||
**Only if** your current AyaNova version is older than 8.2.0 there is an extra step before the normal upgrade process.
|
||||
**Only if** your current AyaNova version is older than 8.3.0 there is an extra step before the normal upgrade process.
|
||||
|
||||
8.2.0 switched to .NET 8 LTS from the prior used .NET 6 LTS and also switched to using Chrome instead of Chromium for the report rendering headless browser so this script accomodates these changes.
|
||||
8.3.0 switched to .NET 10 LTS from the prior used .NET 8 LTS (8.2.0 to 8.2.4), so the .NET 10 runtime must be installed first. This script also sets the folder ownership and permissions the [installation instructions](ops-install-linux-server.md#9-set-ownership-and-permissions) now use: the program files read only for the account AyaNova runs as, which only needs to write to the data directory, and config.json, which holds the database password, readable only by root and AyaNova.
|
||||
|
||||
From a terminal window run the following commands:
|
||||
|
||||
```bash
|
||||
sudo apt-get update && apt-get upgrade && \
|
||||
sudo apt-get install -y aspnetcore-runtime-8.0 && \
|
||||
sudo mkdir /var/ayanova/Chrome && \
|
||||
sudo mkdir /var/ayanova/ChromeHeadlessShell && \
|
||||
sudo apt-get update && sudo apt-get upgrade && \
|
||||
sudo apt-get install -y aspnetcore-runtime-10.0 && \
|
||||
sudo chown -vR :www-data /var/ayanova && \
|
||||
sudo chmod -R g+rwx /var/ayanova/Chrome && \
|
||||
sudo chmod -R g+rwx /var/ayanova/ChromeHeadlessShell && \
|
||||
sudo chmod g+rw /var/ayanova/
|
||||
sudo chmod -R go-w /var/ayanova && \
|
||||
sudo chmod -R g+rw /var/ayanova/data && \
|
||||
sudo chmod 640 /var/ayanova/config.json
|
||||
```
|
||||
|
||||
These commands assume AyaNova runs as `www-data`, as in the installation instructions. If you chose another account, use its group in the `chown` line.
|
||||
|
||||
On Ubuntu 24.04 and 26.04 the .NET 10 runtime comes from Ubuntu's own package archive. On Ubuntu 22.04 first add the Ubuntu .NET backports package repository with `sudo add-apt-repository ppa:dotnet/backports`.
|
||||
|
||||
Installing the .NET 10 runtime replaces the shared `dotnet` command, the .NET 8 runtime stays installed and can be removed later if nothing else on the server uses it.
|
||||
|
||||
If there is an issue or error break apart the commands and execute each one individually to find the error.
|
||||
|
||||
#### Recommended: security settings for the AyaNova service
|
||||
|
||||
The [installation instructions](ops-install-linux-server.md#10-create-and-install-a-service-to-run-ayanova-automatically) now add security settings to the AyaNova service so it can write only to its data directory. To add them to an existing installation without editing the service file, run:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /etc/systemd/system/ayanova.service.d && \
|
||||
printf '[Service]\nNoNewPrivileges=true\nProtectSystem=strict\nReadWritePaths=/var/ayanova/data\nPrivateTmp=true\nProtectHome=true\n' | sudo tee /etc/systemd/system/ayanova.service.d/hardening.conf && \
|
||||
sudo systemctl daemon-reload
|
||||
```
|
||||
|
||||
They take effect when the AyaNova service next starts, in the upgrade step below. If you have set a log, attachments, backup or temporary files path outside of the data directory, first read the note about `ReadWritePaths` in the [installation instructions](ops-install-linux-server.md#10-create-and-install-a-service-to-run-ayanova-automatically).
|
||||
|
||||
If users connect through NGINX as in the [internet access](ops-install-linux-server.md#internet-access) instructions, also set AyaNova to [accept connections only through NGINX](ops-install-linux-server.md#f-accept-connections-only-through-nginx).
|
||||
|
||||
After this step has completed you can continue with the normal upgrade process in the next step.
|
||||
|
||||
### 2\. Upgrade
|
||||
|
||||
#### upgrade script
|
||||
|
||||
The following shell script will automatically stop the AyaNova server service, remove the old AyaNova files, download and unzip the latest AyaNova files then start the server service.
|
||||
The following shell script will automatically stop the AyaNova server service, remove the old AyaNova files, download and unzip the latest AyaNova files then start the server service. It first checks that the .NET 10 runtime is installed and stops without changing anything if it is not (see the pre-upgrade step above).
|
||||
|
||||
Run the following shell commands:
|
||||
|
||||
```bash
|
||||
cd /var/ayanova && \
|
||||
{ dotnet --list-runtimes | grep -q '^Microsoft.AspNetCore.App 10\.' || { echo "The .NET 10 runtime is not installed: do the pre-upgrade step first. Nothing was changed."; false; }; } && \
|
||||
sudo systemctl stop ayanova.service && \
|
||||
sudo curl -O https://www.ayanova.com/download/ayanova-linux-x64-server.zip && \
|
||||
sudo curl -fO https://www.ayanova.com/download/ayanova-linux-x64-server.zip && \
|
||||
sudo rm *.dll && \
|
||||
sudo rm -r ./resource && \
|
||||
sudo rm -r ./wwwroot && \
|
||||
@@ -58,6 +77,12 @@ sudo systemctl start ayanova.service
|
||||
|
||||
If there is an issue or error break apart the commands and execute each one individually to find the error.
|
||||
|
||||
From 8.3.0 the Chrome report rendering engine is downloaded into a `report-browser` folder under the data directory instead of the program folder. After upgrading from an earlier version the old folders are no longer used and can be removed:
|
||||
|
||||
```bash
|
||||
sudo rm -r /var/ayanova/Chrome /var/ayanova/ChromeHeadlessShell
|
||||
```
|
||||
|
||||
AyaNova should restart on it's own and perform any database schema updates if required automatically.
|
||||
|
||||
Open the login form in your browser and after a short delay you should see the new version number at the bottom of the login form.
|
||||
|
||||
@@ -26,17 +26,17 @@ Stop the AyaNova site in the IIS manager:
|
||||
|
||||
### 3.5\. Pre-upgrade step
|
||||
|
||||
If your current AyaNova version is older than 8.2.0 there is an extra step before the normal upgrade script process below.
|
||||
If your current AyaNova version is older than 8.3.0 (8.2.4 or older) there is an extra step before the normal upgrade script process below.
|
||||
|
||||
AyaNova 8.2.0 switched to .NET 8 LTS from the prior used .NET 6 LTS and it must be installed **BEFORE** continuing to the next step.
|
||||
AyaNova 8.3.0 switched to .NET 10 LTS from the prior used .NET 8 LTS and it must be installed **BEFORE** continuing to the next step.
|
||||
|
||||
Install ASP.NET Core 8.x Runtime Hosting Bundle
|
||||
Install ASP.NET Core 10.x Runtime Hosting Bundle
|
||||
|
||||
**Warning** Microsoft provides several different links to "asp.net core runtime" downloads; the one required by AyaNova is the link labelled "Hosting bundle".
|
||||
|
||||
Download the ASP.NET Core **Hosting bundle** runtime here [https://dotnet.microsoft.com/en-us/download/dotnet/8.0](https://dotnet.microsoft.com/en-us/download/dotnet/8.0).
|
||||
Download the ASP.NET Core **Hosting bundle** runtime here [https://dotnet.microsoft.com/en-us/download/dotnet/10.0](https://dotnet.microsoft.com/en-us/download/dotnet/10.0).
|
||||
|
||||
Run the installer and take the defaults to conclusion then proceed to the next step.
|
||||
Run the installer and take the defaults to conclusion, then restart IIS so it picks up the new runtime (run `iisreset` from an administrator command prompt) and proceed to the next step.
|
||||
|
||||
### 4\. Setup
|
||||
|
||||
|
||||
@@ -26,15 +26,15 @@ Alternatively you can force close AyaNova using the ctrl-c key combination in th
|
||||
|
||||
### 3.5\. Pre-upgrade step
|
||||
|
||||
If your current AyaNova version is older than 8.2.0 (8.1.5 or older) there is an extra step before the normal upgrade script process below.
|
||||
If your current AyaNova version is older than 8.3.0 (8.2.4 or older) there is an extra step before the normal upgrade script process below.
|
||||
|
||||
AyaNova 8.2.0 switched to .NET 8 LTS from the prior used .NET 6 LTS and it must be installed **BEFORE** continuing to the next step.
|
||||
AyaNova 8.3.0 switched to .NET 10 LTS from the prior used .NET 8 LTS and it must be installed **BEFORE** continuing to the next step.
|
||||
|
||||
Install ASP.NET Core 8.x Runtime Hosting Bundle
|
||||
Install ASP.NET Core 10.x Runtime Hosting Bundle
|
||||
|
||||
**Warning** Microsoft provides several different links to "asp.net core runtime" downloads; the one required by AyaNova is the link labelled "Hosting bundle".
|
||||
|
||||
Download the ASP.NET Core **Hosting bundle** runtime here [https://dotnet.microsoft.com/en-us/download/dotnet/8.0](https://dotnet.microsoft.com/en-us/download/dotnet/8.0).
|
||||
Download the ASP.NET Core **Hosting bundle** runtime here [https://dotnet.microsoft.com/en-us/download/dotnet/10.0](https://dotnet.microsoft.com/en-us/download/dotnet/10.0).
|
||||
|
||||
Run the installer and take the defaults to conclusion then proceed to the next step.
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
|
||||
[View Change log / release notes](https://ayanova.com/docs/changelog).
|
||||
|
||||
## Upgrading from 8.2.4 or older
|
||||
|
||||
AyaNova 8.3.0 moved to .NET 10. If your AyaNova is version 8.2.4 or older, upgrading a Windows network (IIS or LAN) or Linux installation needs a one-time pre-upgrade step, described in the instructions for your platform below. It applies whichever newer version you upgrade to. Windows single standalone installations need no extra step.
|
||||
|
||||
## Maintenance plan
|
||||
|
||||
AyaNova updates are available with an active Maintenance plan.
|
||||
|
||||
@@ -8,17 +8,17 @@ AyaNova subscription service ensures you are always running the latest version o
|
||||
|
||||
Our network operations engineers will take care of all updates required to AyaNova or the underlying server operating system hosting AyaNova.
|
||||
|
||||
### Update advance notifications
|
||||
### Update notifications
|
||||
|
||||
When an update is required to be installed we will try to give at least 24 hours advance notice however in the case of an urgent security update we may not be able to give any advance warning due to the urgency of the situation.
|
||||
|
||||
An upgrade notification will be emailed to the email address used to purchase the AyaNova subscription service. The notification will contain information about the type of upgrade required and a link to the [AyaNova change log](changelog.md) so you can see exactly what will be fixed, changed or new features added.
|
||||
When your service has been upgraded to a new version of AyaNova, a notification is emailed to the email address used to purchase the AyaNova subscription service. It contains a link to the [AyaNova change log](changelog.md) so you can see exactly what was fixed, changed or added.
|
||||
|
||||
### Downtime during an update
|
||||
|
||||
Standard upgrades to AyaNova typically require less than one minute of down time where the AyaNova server will be unresponsive. During an update there is typically no need to log out of AyaNova, there will be a brief period of non responsiveness and then AyaNova will continue to respond normally.
|
||||
Every week the operating system of the server hosting your AyaNova is updated, and the server is then restarted: AyaNova is unavailable for as much as 2 minutes.
|
||||
|
||||
In the case of an operating system upgrade the server itself hosting your AyaNova instance needs to be rebooted so there can be as much as 2 minutes down time on average. Again there is typically no need to log out of AyaNova, it is designed to allow you to continue working once it has reloaded and is ready for connections.
|
||||
When a new version of AyaNova is installed, which is less frequent than the weekly update, the server is shut down for the upgrade, so AyaNova can be unavailable for a few minutes longer, usually 5 minutes or less.
|
||||
|
||||
In either case there is no need to log out of AyaNova: it stops responding for a short time and then continues normally once it is ready for connections again.
|
||||
|
||||
### Web application update pop-up
|
||||
|
||||
@@ -28,13 +28,14 @@ If you choose to do it later you can continue to work and then select to reload
|
||||
|
||||
### What time will my service be upgraded?
|
||||
|
||||
We have chosen standard times for each data center to install standard updates that are intended to be outside of normal business hours relative to each data center to avoid any disruption to your business.
|
||||
We have chosen standard times for each data center to install updates, outside of normal business hours relative to each data center to avoid any disruption to your business.
|
||||
|
||||
Usually, if necessary, we will perform maintenance or upgrades on Thursday (our west coast north america time zone).
|
||||
Updates are installed every Thursday (Pacific time, our time zone).
|
||||
|
||||
Your service will be updated at the following data center local time:
|
||||
|
||||
- New York 7 p.m.
|
||||
- Atlanta 7 p.m.
|
||||
- San Francisco 7 p.m.
|
||||
- Amsterdam 1 a.m.
|
||||
- Singapore 1 a.m.
|
||||
@@ -44,7 +45,7 @@ Your service will be updated at the following data center local time:
|
||||
- Bangalore 5:30 a.m.
|
||||
- Sydney 4 a.m.
|
||||
|
||||
Note that in the case of an urgent or emergency security update we may be forced to install it immediately at any time of day without advance notice.
|
||||
An urgent update, such as a security fix, may be installed on any day and at any time if necessary; we always try to keep the impact on your business as small as possible.
|
||||
|
||||
## Backups
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ Read only access
|
||||
- Service manager - restricted
|
||||
- Service technician
|
||||
|
||||
Customer contacts with the Customer role can create Customer service requests for their own Customer and view them; with the Customer - restricted role they can view them.
|
||||
|
||||
## How to access Customer service requests
|
||||
|
||||
Customer service requests are accessed in the following ways:
|
||||
@@ -115,6 +117,8 @@ This option will locally create a new work order based off this Customer service
|
||||
|
||||
If the Work order is not saved no changes are made to the Customer service request and it remains Open.
|
||||
|
||||
If that work order, or the work order item created from the request, is later deleted, the Customer service request goes back to Open so it can be accepted again. No notification is sent for this.
|
||||
|
||||
#### Reject service request
|
||||
|
||||
This option will immediately reject the CSR, save changes and close the CSR form.
|
||||
|
||||
@@ -628,7 +628,7 @@ All Work order item Scheduled User edit form fields here are **optional** but it
|
||||
|
||||
This field is a [date time field](ay-start-common-controls.md#date-and-time) control and is used to enter the scheduled point in time when service should begin.
|
||||
|
||||
If the [Global settings Scheduled / labor default minutes](adm-global-settings.md#scheduled--labor-default-minutes) value is set, AyaNova will automatically set the _Stop date time_ and _Estimated quantity_ on new records.
|
||||
If the [Global settings Scheduled / labor default minutes](adm-global-settings.md#scheduled-labor-default-minutes) value is set, AyaNova will automatically set the _Stop date time_ and _Estimated quantity_ on new records.
|
||||
|
||||
If a stop time is already set, adjusting the start time will cause AyaNova to calculate the _Estimated Quantity_ field automatically.
|
||||
|
||||
@@ -640,7 +640,7 @@ AyaNova has a feature to prevent scheduling a technician more than once within t
|
||||
|
||||
This field is a [date time field](ay-start-common-controls.md#date-and-time) control and is used to enter the scheduled point in time when service should stop.
|
||||
|
||||
If the [Global settings Scheduled / labor default minutes](adm-global-settings.md#scheduled--labor-default-minutes) value is set, and the _Start date time_ field is empty, AyaNova will automatically set the _Start date time_ and _Estimated quantity_ on new records.
|
||||
If the [Global settings Scheduled / labor default minutes](adm-global-settings.md#scheduled-labor-default-minutes) value is set, and the _Start date time_ field is empty, AyaNova will automatically set the _Start date time_ and _Estimated quantity_ on new records.
|
||||
|
||||
If a start time is already set, adjusting the stop time will cause AyaNova to calculate the _Estimated Quantity_ field automatically.
|
||||
|
||||
@@ -1024,7 +1024,7 @@ All Work order item labor edit form fields here are **optional**.
|
||||
|
||||
This field is a [date time field](ay-start-common-controls.md#date-and-time) control and is used to enter the point in time when labor began.
|
||||
|
||||
If the [Global settings Scheduled / labor default minutes](adm-global-settings.md#scheduled--labor-default-minutes) value is set, AyaNova will automatically set the _Service Stop date time_ and _Quantity_ on new records.
|
||||
If the [Global settings Scheduled / labor default minutes](adm-global-settings.md#scheduled-labor-default-minutes) value is set, AyaNova will automatically set the _Service Stop date time_ and _Quantity_ on new records.
|
||||
|
||||
If a stop time is already set, adjusting the start time will cause AyaNova to calculate the _Quantity_ field automatically.
|
||||
|
||||
@@ -1032,7 +1032,7 @@ If a stop time is already set, adjusting the start time will cause AyaNova to ca
|
||||
|
||||
This field is a [date time field](ay-start-common-controls.md#date-and-time) control and is used to enter the point in time when labor ended.
|
||||
|
||||
If the [Global settings Scheduled / labor default minutes](adm-global-settings.md#scheduled--labor-default-minutes) value is set, and the _Service Start date time_ field is empty, AyaNova will automatically set the _Service Start date time_ and _Quantity_ on new records.
|
||||
If the [Global settings Scheduled / labor default minutes](adm-global-settings.md#scheduled-labor-default-minutes) value is set, and the _Service Start date time_ field is empty, AyaNova will automatically set the _Service Start date time_ and _Quantity_ on new records.
|
||||
|
||||
If a start time is already set, adjusting the stop time will cause AyaNova to calculate the _Quantity_ field automatically.
|
||||
|
||||
|
||||
@@ -170,7 +170,7 @@ It's best to enable custom fields in coordination with the other Users or after
|
||||
|
||||
### Reporting and printing Custom fields
|
||||
|
||||
If you want to generate reports that contain the custom fields added you can do that via the [report template editor](ay-report-edit#custom-fields-in-report-data).
|
||||
If you want to generate reports that contain the custom fields added you can do that via the [report template editor](ay-report-edit.md#custom-fields-in-report-data).
|
||||
|
||||
### Disable custom fields
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ It is **critically important** to do this before taking any further steps; parti
|
||||
|
||||
Failing to do this would mean you will have no effective security as the AyaNova superuser password is well known and could be used by anyone to cause significant damage to your data or create more Users with administrative level privileges.
|
||||
|
||||
Once AyaNova is licensed, if you log in as the superuser with the default password `l3tm3in`, you will be taken automatically to the change password form to encourage it to be changed.
|
||||
Once AyaNova is licensed, if you log in as the superuser with the default password `l3tm3in`, you will be taken automatically to the change password form, and the rest of AyaNova can not be used by the superuser until the password is changed.
|
||||
|
||||
We have [secure password guidelines](home-password.md#what-is-a-secure-password) for the latest best practices in choosing a secure password and we encourage you to read about it and consider the advice written there in order to choose an easy to memorize, secure password that does not need to be written down or stored insecurely.
|
||||
|
||||
@@ -140,7 +140,7 @@ We have a full [backup guide](ops-form-backup.md) that covers how to set up the
|
||||
|
||||
Read that guide to learn important safety precautions such as why it's not good enough to setup an automated backup and forget about it, the data **must be stored off-site**.
|
||||
|
||||
Also be sure to read and follow the important [advice regarding setting up](ops-form-backup.md#assign-a-staff-member-the-operations---restricted-role) a regular non technical staff member with the `Operations - restricted` Role.
|
||||
Also be sure to read and follow the important [advice regarding setting up](ops-form-backup.md#assign-a-staff-member-the-operations-restricted-role) a regular non technical staff member with the `Operations - restricted` Role.
|
||||
|
||||
Once this step is completed and you are confident your backup process is in place and your data secure it's safe to proceed.
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ Once you have access to AyaNova and some sample data generated to play with we r
|
||||
AyaNova has a built in [import feature](adm-import.md) which enables you to import data from text files which you should try out with real data as part of your evaluation process.
|
||||
|
||||
**Importing from QuickBooks**
|
||||
If you use QuickBooks desktop edition you can import data directly from QuickBooks using our [QBI QuickBooks integration utility](integration-qbd) (more accounting integrations are in the works, contact us if there is a specific one of interest, we may already be working on it).
|
||||
If you use QuickBooks desktop edition you can import data directly from QuickBooks using our [QBI QuickBooks integration utility](integration-qbd.md) (more accounting integrations are in the works, contact us if there is a specific one of interest, we may already be working on it).
|
||||
|
||||
**Importing from older versions of AyaNova**
|
||||
If you are using an older version of AyaNova we have a [migration utility](ay-biz-admin-migrate-v7.md) that will **import legacy AyaNova data** to version 8.
|
||||
|
||||
@@ -8,7 +8,10 @@ site_name: AyaNova manual
|
||||
site_dir: "../../../server/AyaNova/wwwroot/docs"
|
||||
site_url: https://ayanova.com/docs/
|
||||
strict: true
|
||||
copyright: Copyright © 2022-2025 Ground Zero Tech-Works Inc. REV-2025-11-24
|
||||
# YEAR-AUTO and REV-AUTO are set when the manual is built (../rev_date.py)
|
||||
copyright: Copyright © 2022-YEAR-AUTO Ground Zero Tech-Works Inc. REV-AUTO
|
||||
hooks:
|
||||
- ../rev_date.py
|
||||
extra:
|
||||
generator: false
|
||||
# Extensions
|
||||
@@ -197,8 +200,10 @@ nav:
|
||||
- "Reporting render browser path": "ops-config-report-render-browser-path.md"
|
||||
- "Reporting timeout configuration": "ops-config-report-rendering-timeout.md"
|
||||
- "Environment variable reference": "ops-config-environment-variables.md"
|
||||
- "SMTP certificate check": "ops-config-smtp-accept-any-certificate.md"
|
||||
- "Security and JWT web tokens": "ops-config-jwt-secret.md"
|
||||
- "SuperUser password reset": "ops-config-set-superuser-pw.md"
|
||||
- "Trusted reverse proxies": "ops-config-trusted-proxies.md"
|
||||
- "Server error codes": "ops-error-codes.md"
|
||||
- "Migrating from AyaNova 7": "ops-import-v7.md"
|
||||
- "Metrics": "ops-metrics.md"
|
||||
|
||||
@@ -8,7 +8,10 @@ theme:
|
||||
site_name: AyaNova Customer user manual
|
||||
site_dir: '../../../server/AyaNova/wwwroot/cust'
|
||||
strict: true
|
||||
copyright: Copyright © 2022 Ground Zero Tech-Works Inc. REV-2022-07-18
|
||||
# YEAR-AUTO and REV-AUTO are set when the manual is built (../rev_date.py)
|
||||
copyright: Copyright © 2022-YEAR-AUTO Ground Zero Tech-Works Inc. REV-AUTO
|
||||
hooks:
|
||||
- ../rev_date.py
|
||||
extra:
|
||||
generator: false
|
||||
# Extensions
|
||||
|
||||
34
docs/8.0/rev_date.py
Normal file
34
docs/8.0/rev_date.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# case 4757: the revision stamp and copyright year in the footer are set when a manual is built, never by hand.
|
||||
# Used by both manuals (hooks: in mkdocs.yml). In the copyright line REV-AUTO becomes REV-<date> and YEAR-AUTO
|
||||
# becomes that date's year. The date is the last commit that changed this manual (its folder, mkdocs.yml included),
|
||||
# or today if it has uncommitted changes or git is not available (e.g. a build outside the repo).
|
||||
# Both manuals build with strict: true, so a warning here fails the build: only a missing placeholder warns.
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
log = logging.getLogger("mkdocs.hooks.rev_date")
|
||||
|
||||
|
||||
def _git(folder, *args):
|
||||
return subprocess.run(["git", *args, "--", "."], cwd=folder, capture_output=True, text=True, check=True).stdout.strip()
|
||||
|
||||
|
||||
def on_config(config):
|
||||
folder = os.path.dirname(os.path.abspath(config.config_file_path))
|
||||
today = datetime.date.today().isoformat()
|
||||
try:
|
||||
rev = today if _git(folder, "status", "--porcelain") else _git(folder, "log", "-1", "--format=%cs")
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
log.info("git not available: revision date set to today")
|
||||
rev = today
|
||||
rev = rev or today
|
||||
|
||||
copyright = config.copyright or ""
|
||||
for placeholder in ("REV-AUTO", "YEAR-AUTO"):
|
||||
if placeholder not in copyright:
|
||||
log.warning(f"{placeholder} not found in the copyright line of {config.config_file_path}")
|
||||
config.copyright = copyright.replace("REV-AUTO", "REV-" + rev).replace("YEAR-AUTO", rev[:4])
|
||||
log.info(f"revision stamp: {config.copyright}")
|
||||
return config
|
||||
@@ -1,11 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<GenerateFullPaths>true</GenerateFullPaths>
|
||||
<Version>8.2.4</Version>
|
||||
<FileVersion>8.2.4.0</FileVersion>
|
||||
<Version>8.3.1</Version>
|
||||
<FileVersion>8.3.1.0</FileVersion>
|
||||
<ApplicationIcon>ayanova.ico</ApplicationIcon>
|
||||
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||
<noWarn>1591</noWarn>
|
||||
@@ -24,40 +24,41 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bogus" Version="35.5.1" />
|
||||
<PackageReference Include="BouncyCastle.Cryptography" Version="2.3.1" />
|
||||
<PackageReference Include="Bogus" Version="35.6.5" />
|
||||
<PackageReference Include="BouncyCastle.Cryptography" Version="2.7.0" />
|
||||
<PackageReference Include="Enums.NET" Version="5.0.0" />
|
||||
<PackageReference Include="jose-jwt" Version="5.0.0" />
|
||||
<PackageReference Include="MailKit" Version="4.5.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.4" />
|
||||
<PackageReference Include="jose-jwt" Version="5.3.0" />
|
||||
<PackageReference Include="MailKit" Version="4.18.1" />
|
||||
<!-- Not used directly: pinned because MimeKit only requires 10.0.0, which has security advisories (NU1902/NU1903) -->
|
||||
<PackageReference Include="System.Security.Cryptography.Pkcs" Version="10.0.13" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.13" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.13" />
|
||||
<!-- Used directly throughout; referenced explicitly rather than only through Mvc.NewtonsoftJson (which sets a 13.0.3 floor, NU1605) -->
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
|
||||
<!-- <PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer" Version="5.0.0" /> -->
|
||||
<PackageReference Include="Asp.Versioning.Mvc" Version="8.1.0" />
|
||||
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
||||
<PackageReference Include="Asp.Versioning.Mvc" Version="10.2.1" />
|
||||
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="10.2.1" />
|
||||
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="8.0.4">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.13" />
|
||||
<PackageReference Include="Microsoft.Extensions.ApiDescription.Server" Version="10.0.13">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.8" />
|
||||
<PackageReference Include="Microsoft.OpenApi" Version="1.6.14" />
|
||||
<PackageReference Include="MiniProfiler.AspNetCore.Mvc" Version="4.3.8" />
|
||||
<PackageReference Include="MiniProfiler.EntityFrameworkCore" Version="4.3.8" />
|
||||
<PackageReference Include="NLog" Version="5.3.2" />
|
||||
<PackageReference Include="NLog.Web.AspNetCore" Version="5.3.11" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
|
||||
<PackageReference Include="PuppeteerSharp" Version="17.0.0" />
|
||||
<PackageReference Include="QRCoder" Version="1.5.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="6.5.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="6.5.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.5.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUi" Version="6.5.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="6.5.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="10.0.13" />
|
||||
<PackageReference Include="NLog" Version="6.2.2" />
|
||||
<PackageReference Include="NLog.Web.AspNetCore" Version="6.2.2" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
|
||||
<PackageReference Include="PuppeteerSharp" Version="25.15.0" />
|
||||
<PackageReference Include="QRCoder" Version="1.8.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.3.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="10.3.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="10.3.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="10.3.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUi" Version="10.3.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Newtonsoft" Version="10.3.1" />
|
||||
<PackageReference Include="TwoFactorAuth.Net" Version="1.4.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ namespace AyaNova
|
||||
using Asp.Versioning.ApiExplorer;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Microsoft.OpenApi;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using System;
|
||||
|
||||
|
||||
107
server/AyaNova/ControllerHelpers/CustomerContactRoutes.cs
Normal file
107
server/AyaNova/ControllerHelpers/CustomerContactRoutes.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
namespace AyaNova.Api.ControllerHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// case 4712: the only API routes a customer contact (UserType Customer or HeadOffice) may call:
|
||||
/// the ones the customer screens of the client use. Everything else answers 403 to a contact, so a
|
||||
/// new route stays closed to contacts unless it is added here on purpose.
|
||||
/// Ownership of the records these routes return is checked in the routes themselves.
|
||||
/// See devdocs/modernization/SECURITY-REVIEW.md, "Customer contact route allowlist".
|
||||
/// </summary>
|
||||
internal static class CustomerContactRoutes
|
||||
{
|
||||
private const string API_PREFIX = "api/v{version:apiVersion}/";
|
||||
|
||||
//"METHOD route template" with the API prefix removed, exactly as in the controllers' attributes
|
||||
private static readonly HashSet<string> Allowed = new HashSet<string>
|
||||
{
|
||||
//own account
|
||||
"POST auth/change-password",
|
||||
"GET auth/totp",
|
||||
"POST auth/totp-validate",
|
||||
"POST auth/totp-disable/{id}",
|
||||
"GET user-option/{id}",
|
||||
"PUT user-option/{id}",
|
||||
//app shell
|
||||
"GET global-biz-setting/client",
|
||||
"GET enum-list/list/{enumkey}",
|
||||
"GET form-custom/{formkey}",
|
||||
"GET translation/list",
|
||||
"POST translation/subset",
|
||||
"GET server-info",
|
||||
"GET notify/new-count",
|
||||
"GET notify/app-notifications",
|
||||
"DELETE notify/{id}",
|
||||
//data lists (the server sets the customer filter for contacts)
|
||||
"POST data-list",
|
||||
"GET data-list/listfields",
|
||||
"GET data-list-column-view/{listKey}",
|
||||
"POST data-list-column-view",
|
||||
"DELETE data-list-column-view/{listKey}",
|
||||
"POST data-list-column-view/sort",
|
||||
"GET data-list-filter/list",
|
||||
"GET data-list-filter/{id}",
|
||||
"POST data-list-filter",
|
||||
"PUT data-list-filter",
|
||||
"DELETE data-list-filter/{id}",
|
||||
"GET tag-list/list",
|
||||
"POST pick-list/list",
|
||||
"POST pick-list/single",
|
||||
//work order (customer view, customer report, attachments)
|
||||
"GET workorder/customer/{id}",
|
||||
"GET attachment/list",
|
||||
"POST report/render-job",
|
||||
"GET job-operations/status/{gid}",
|
||||
"GET job-operations/logs/{gid}",
|
||||
"POST report/request-cancel/{gid}",
|
||||
//customer service requests (not accept / reject, which are staff actions)
|
||||
"POST csr",
|
||||
"PUT csr",
|
||||
"GET csr/{id}",
|
||||
"DELETE csr/{id}",
|
||||
"POST unit/customerunit",
|
||||
//notification subscriptions
|
||||
"GET notify-subscription/list",
|
||||
"GET notify-subscription/{id}",
|
||||
"POST notify-subscription",
|
||||
"PUT notify-subscription",
|
||||
"DELETE notify-subscription/{id}",
|
||||
"GET work-order-status/list",
|
||||
"GET quote-status/list",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// True if a customer contact may call the endpoint this request was routed to.
|
||||
/// Anonymous routes (login, downloads by key, logo...) are public anyway and always allowed.
|
||||
/// </summary>
|
||||
internal static bool IsAllowed(HttpContext context)
|
||||
{
|
||||
var endpoint = context.GetEndpoint();
|
||||
if (endpoint == null)
|
||||
return true;//not routed to anything: the request ends in a 404 regardless
|
||||
if (endpoint.Metadata.GetMetadata<IAllowAnonymous>() != null)
|
||||
return true;
|
||||
var key = RouteKey(context);
|
||||
return key != null && Allowed.Contains(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// "METHOD route template" of the endpoint this request was routed to, API prefix removed
|
||||
/// (e.g. "GET user-option/{id}"); null when not routed to a route endpoint.
|
||||
/// Also used by the token middleware (case 4725)
|
||||
/// </summary>
|
||||
internal static string RouteKey(HttpContext context)
|
||||
{
|
||||
if (context.GetEndpoint() is not RouteEndpoint routeEndpoint)
|
||||
return null;
|
||||
var template = routeEndpoint.RoutePattern.RawText ?? string.Empty;
|
||||
if (template.StartsWith(API_PREFIX))
|
||||
template = template.Substring(API_PREFIX.Length);
|
||||
return $"{context.Request.Method.ToUpperInvariant()} {template}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace AyaNova.Api.ControllerHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// case 4725: with a purchased licence, a SuperUser still using the default password may only change it:
|
||||
/// these are the routes the change password page and the app shell need. Everything else answers 403
|
||||
/// until the password is changed.
|
||||
/// See devdocs/modernization/SECURITY-REVIEW.md, F9.
|
||||
/// </summary>
|
||||
internal static class SuperUserDefaultPasswordRoutes
|
||||
{
|
||||
//"METHOD route template" with the API prefix removed, exactly as in the controllers' attributes
|
||||
private static readonly HashSet<string> Allowed = new HashSet<string>
|
||||
{
|
||||
"POST auth/change-password",
|
||||
"GET user-option/{id}",
|
||||
"GET global-biz-setting/client",
|
||||
"GET enum-list/list/{enumkey}",
|
||||
"GET form-custom/{formkey}",
|
||||
"GET translation/list",
|
||||
"POST translation/subset",
|
||||
"GET server-info",
|
||||
"GET notify/new-count",
|
||||
"GET notify/app-notifications",
|
||||
};
|
||||
|
||||
internal static bool IsAllowed(HttpContext context)
|
||||
{
|
||||
var endpoint = context.GetEndpoint();
|
||||
if (endpoint == null)
|
||||
return true;//not routed to anything: the request ends in a 404 regardless
|
||||
if (endpoint.Metadata.GetMetadata<IAllowAnonymous>() != null)
|
||||
return true;
|
||||
var key = CustomerContactRoutes.RouteKey(context);
|
||||
return key != null && Allowed.Contains(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,9 +47,9 @@ namespace AyaNova.Api.Controllers
|
||||
*/
|
||||
|
||||
|
||||
var dbId = Uri.EscapeDataString(AyaNova.Core.License.ServerDbId);
|
||||
var company = Uri.EscapeDataString(AyaNova.Core.License.ActiveKey.RegisteredTo);
|
||||
return $"https://contact.ayanova.com/contact?dbid={dbId}&company={company}";
|
||||
//case 4727 (F11, F39): this page is anonymous, so the link no longer carries the DB ID and licensee;
|
||||
//the About form's support link (logged in) still fills them in
|
||||
return "https://contact.ayanova.com/contact";
|
||||
}
|
||||
|
||||
|
||||
@@ -98,7 +98,6 @@ namespace AyaNova.Api.Controllers
|
||||
<h2><a href=""/docs"" target=""_blank"">User and technical guide</a></h2>
|
||||
<h2><a href=""{SupportUrl()}"" target=""_blank"">Contact technical support</a></h2>
|
||||
<h2><a href=""/api-docs"" target=""_blank"">API explorer for developers</a></h2>
|
||||
<pre>{AyaNova.Core.License.LicenseInfo}</pre>
|
||||
</div>
|
||||
</body>
|
||||
</html>";
|
||||
@@ -120,6 +119,9 @@ namespace AyaNova.Api.Controllers
|
||||
[HttpGet("server-info")]
|
||||
public ActionResult ServerInfo()
|
||||
{
|
||||
//case 4727 (F29): the DB ID and licence go to staff only (Service, NotService), as the About form shows them
|
||||
var userType = UserTypeFromContext.Type(HttpContext.Items);
|
||||
bool staff = userType == UserType.Service || userType == UserType.NotService;
|
||||
return Ok(new
|
||||
{
|
||||
data = new
|
||||
@@ -128,8 +130,8 @@ namespace AyaNova.Api.Controllers
|
||||
DBSchemaVersion = AySchema.currentSchema,
|
||||
ServerLocalTime = DateUtil.ServerDateTimeString(System.DateTime.UtcNow),
|
||||
ServerTimeZone = TimeZoneInfo.Local.Id,
|
||||
ServerDbId = AyaNova.Core.License.ServerDbId,
|
||||
License = AyaNova.Core.License.LicenseInfoAsJson,
|
||||
ServerDbId = staff ? AyaNova.Core.License.ServerDbId : string.Empty,
|
||||
License = staff ? AyaNova.Core.License.LicenseInfoAsJson : null,
|
||||
#if (SUBSCRIPTION_BUILD)
|
||||
Build = "Subscription"
|
||||
#else
|
||||
|
||||
@@ -157,11 +157,16 @@ namespace AyaNova.Api.Controllers
|
||||
|
||||
//Is this a customer user attempting to view a wo attachments??
|
||||
var userType = UserTypeFromContext.Type(HttpContext.Items);
|
||||
if (ayaType == AyaType.WorkOrder && (userType == UserType.Customer || userType == UserType.HeadOffice))
|
||||
//case 4712: customer contacts see attachments only on their own customer's work orders
|
||||
if (userType == UserType.Customer || userType == UserType.HeadOffice)
|
||||
{
|
||||
if (ayaType != AyaType.WorkOrder)
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
var wo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == ayaId).Select(x => new { x.Tags, x.CustomerId }).FirstOrDefaultAsync();
|
||||
if (wo == null || !await UserBiz.CustomerContactOwnsCustomerAsync(ct, UserIdFromContext.Id(HttpContext.Items), wo.CustomerId))
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
//check if allowed
|
||||
var woTags = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == ayaId).Select(x => x.Tags).FirstOrDefaultAsync();
|
||||
var custUserRights = await UserBiz.CustomerUserEffectiveRightsAsync(UserIdFromContext.Id(HttpContext.Items), woTags);
|
||||
var custUserRights = await UserBiz.CustomerUserEffectiveRightsAsync(UserIdFromContext.Id(HttpContext.Items), wo.Tags);
|
||||
if (!custUserRights.ThisWOCanAttachments)
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
}
|
||||
@@ -264,7 +269,9 @@ namespace AyaNova.Api.Controllers
|
||||
{
|
||||
AttachToAType = uploadFormData.FormFieldData["AttachToAType"].ToString();
|
||||
//for v8 migrate purposes
|
||||
if (uploadFormData.FormFieldData.ContainsKey("OverrideUserId"))
|
||||
//case 4727 (F25): only SuperUser or a user who may manage users may set it, otherwise it is ignored
|
||||
if (uploadFormData.FormFieldData.ContainsKey("OverrideUserId") &&
|
||||
(UserIdFromContext.Id(HttpContext.Items) == 1 || Authorized.HasModifyRole(HttpContext.Items, AyaType.User)))
|
||||
OverrideUserId = long.Parse(uploadFormData.FormFieldData["OverrideUserId"].ToString());
|
||||
|
||||
AttachToObjectId = uploadFormData.FormFieldData["AttachToObjectId"].ToString();
|
||||
@@ -553,11 +560,16 @@ namespace AyaNova.Api.Controllers
|
||||
//is this allowed?
|
||||
//Is this a customer user attempting to view a wo attachments??
|
||||
|
||||
if (dbObject.AttachToAType == AyaType.WorkOrder && (DownloadUser.UserType == UserType.Customer || DownloadUser.UserType == UserType.HeadOffice))
|
||||
//case 4712: customer contacts download attachments only from their own customer's work orders
|
||||
if (DownloadUser.UserType == UserType.Customer || DownloadUser.UserType == UserType.HeadOffice)
|
||||
{
|
||||
if (dbObject.AttachToAType != AyaType.WorkOrder)
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
var wo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == dbObject.AttachToObjectId).Select(x => new { x.Tags, x.CustomerId }).FirstOrDefaultAsync();
|
||||
if (wo == null || !await UserBiz.CustomerContactOwnsCustomerAsync(ct, DownloadUser.Id, wo.CustomerId))
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
//check if allowed
|
||||
var woTags = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == dbObject.AttachToObjectId).Select(x => x.Tags).FirstOrDefaultAsync();
|
||||
var custUserRights = await UserBiz.CustomerUserEffectiveRightsAsync(DownloadUser.Id, woTags);
|
||||
var custUserRights = await UserBiz.CustomerUserEffectiveRightsAsync(DownloadUser.Id, wo.Tags);
|
||||
if (!custUserRights.ThisWOCanAttachments)
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -34,6 +35,10 @@ namespace AyaNova.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>
|
||||
@@ -77,8 +82,14 @@ namespace AyaNova.Api.Controllers
|
||||
return StatusCode(451, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED, "generalerror", "License agreement consent required"));
|
||||
}
|
||||
|
||||
//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(AyaNova.Util.ServerBootConfig.FAILED_AUTH_DELAY);
|
||||
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
||||
@@ -94,7 +105,7 @@ namespace AyaNova.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
|
||||
@@ -103,6 +114,7 @@ namespace AyaNova.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" };
|
||||
@@ -128,11 +140,20 @@ namespace AyaNova.Api.Controllers
|
||||
}
|
||||
|
||||
//No users matched, it's a failed login
|
||||
RecordLoginFailure(creds.Login, address);
|
||||
//Make a failed pw wait
|
||||
await Task.Delay(AyaNova.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");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -167,7 +188,14 @@ namespace AyaNova.Api.Controllers
|
||||
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
||||
}
|
||||
|
||||
//Match to temp token that would have been set by initial credentialed login for 2fa User
|
||||
//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(AyaNova.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();
|
||||
|
||||
|
||||
@@ -184,14 +212,22 @@ namespace AyaNova.Api.Controllers
|
||||
var tfa = new TwoFactorAuth("AyaNova");
|
||||
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(AyaNova.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);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -52,6 +54,12 @@ namespace AyaNova.Api.Controllers
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
if (!ModelState.IsValid)
|
||||
return BadRequest(new ApiErrorResponse(ModelState));
|
||||
if (!await ContactMayUseCustomerAsync(newObject.CustomerId))
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
//case 4712: a contact's request is always requested by that contact
|
||||
var userType = UserTypeFromContext.Type(HttpContext.Items);
|
||||
if (userType == UserType.Customer || userType == UserType.HeadOffice)
|
||||
newObject.RequestedByUserId = UserIdFromContext.Id(HttpContext.Items);
|
||||
CustomerServiceRequest o = await biz.CreateAsync(newObject);
|
||||
if (o == null)
|
||||
return BadRequest(new ApiErrorResponse(biz.Errors));
|
||||
@@ -76,6 +84,8 @@ namespace AyaNova.Api.Controllers
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
if (!ModelState.IsValid)
|
||||
return BadRequest(new ApiErrorResponse(ModelState));
|
||||
if (!await ContactMayUseExistingAsync(id))
|
||||
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
|
||||
var o = await biz.GetAsync(id);
|
||||
if (o == null) return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
|
||||
return Ok(ApiOkResponse.Response(o));
|
||||
@@ -96,7 +106,11 @@ namespace AyaNova.Api.Controllers
|
||||
CustomerServiceRequestBiz biz = CustomerServiceRequestBiz.GetBiz(ct, HttpContext);
|
||||
if (!Authorized.HasModifyRole(HttpContext.Items, biz.BizType))
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
var o = await biz.PutAsync(updatedObject);
|
||||
if (!await ContactMayUseExistingAsync(updatedObject.Id))
|
||||
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
|
||||
if (!await ContactMayUseCustomerAsync(updatedObject.CustomerId))
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
var o = await biz.PutAsync(updatedObject);
|
||||
if (o == null)
|
||||
{
|
||||
if (biz.Errors.Exists(z => z.Code == ApiErrorCode.CONCURRENCY_CONFLICT))
|
||||
@@ -122,11 +136,32 @@ namespace AyaNova.Api.Controllers
|
||||
CustomerServiceRequestBiz biz = CustomerServiceRequestBiz.GetBiz(ct, HttpContext);
|
||||
if (!Authorized.HasDeleteRole(HttpContext.Items, biz.BizType))
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
if (!await ContactMayUseExistingAsync(id))
|
||||
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
|
||||
if (!await biz.DeleteAsync(id))
|
||||
return BadRequest(new ApiErrorResponse(biz.Errors));
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
//case 4712: a customer contact works only with its own customer's service requests (staff: always true)
|
||||
private async Task<bool> ContactMayUseCustomerAsync(long customerId)
|
||||
{
|
||||
var userType = UserTypeFromContext.Type(HttpContext.Items);
|
||||
if (userType != UserType.Customer && userType != UserType.HeadOffice)
|
||||
return true;
|
||||
return await UserBiz.CustomerContactOwnsCustomerAsync(ct, UserIdFromContext.Id(HttpContext.Items), customerId);
|
||||
}
|
||||
|
||||
//case 4712: same for an existing service request (true if it does not exist, so the normal not found applies)
|
||||
private async Task<bool> ContactMayUseExistingAsync(long id)
|
||||
{
|
||||
var userType = UserTypeFromContext.Type(HttpContext.Items);
|
||||
if (userType != UserType.Customer && userType != UserType.HeadOffice)
|
||||
return true;
|
||||
var customerId = await ct.CustomerServiceRequest.AsNoTracking().Where(z => z.Id == id).Select(z => (long?)z.CustomerId).FirstOrDefaultAsync();
|
||||
return customerId == null || await UserBiz.CustomerContactOwnsCustomerAsync(ct, UserIdFromContext.Id(HttpContext.Items), (long)customerId);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -80,7 +80,8 @@ namespace AyaNova.Api.Controllers
|
||||
log.LogDebug($"Instantiating biz object handler for {selectedRequest.AType}");
|
||||
var biz = BizObjectFactory.GetBizObject(selectedRequest.AType, ct, UserId, UserRoles, UserTranslationId);
|
||||
log.LogDebug($"Fetching data for {selectedRequest.SelectedRowIds.Length} {selectedRequest.AType} items");
|
||||
string baseFileName = FileUtil.StringToSafeFileName($"{selectedRequest.AType.ToString().ToLowerInvariant()}-{FileUtil.GetSafeDateFileName()}");
|
||||
//case 4727 (F25): the name carries the user's id, so only that user can download it
|
||||
string baseFileName = FileUtil.StringToSafeFileName($"{selectedRequest.AType.ToString().ToLowerInvariant()}-u{UserId}-{FileUtil.GetSafeDateFileName()}");
|
||||
string outputSourceFileName = baseFileName + ".json";
|
||||
string outputSourceFullPath = System.IO.Path.Combine(FileUtil.TemporaryFilesFolder, outputSourceFileName);
|
||||
|
||||
@@ -130,13 +131,15 @@ namespace AyaNova.Api.Controllers
|
||||
if (!serverState.IsOpen)
|
||||
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
|
||||
|
||||
if (await UserBiz.ValidateDownloadTokenAndReturnUserAsync(t, ct) == null)
|
||||
var DownloadUser = await UserBiz.ValidateDownloadTokenAndReturnUserAsync(t, ct);
|
||||
if (DownloadUser == null)
|
||||
{
|
||||
await Task.Delay(ServerBootConfig.FAILED_AUTH_DELAY);//DOS protection
|
||||
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
||||
}
|
||||
|
||||
if (!FileUtil.TemporaryFileExists(fileName))
|
||||
//case 4727 (F25): only the caller's own export file (see the name in render), no other temporary file
|
||||
if (fileName == null || !fileName.Contains($"-u{DownloadUser.Id}-") || !fileName.EndsWith(".json") || !FileUtil.TemporaryFileExists(fileName))
|
||||
{
|
||||
await Task.Delay(ServerBootConfig.FAILED_AUTH_DELAY);//fishing protection
|
||||
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
|
||||
|
||||
@@ -109,6 +109,10 @@ namespace AyaNova.Api.Controllers
|
||||
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
|
||||
}
|
||||
|
||||
//case 4727 (F29): the DB ID and licensee go to staff only (Service, NotService)
|
||||
var userType = UserTypeFromContext.Type(HttpContext.Items);
|
||||
bool staff = userType == UserType.Service || userType == UserType.NotService;
|
||||
|
||||
bool IsSubscriptionBuild = false;
|
||||
|
||||
#if (SUBSCRIPTION_BUILD)
|
||||
@@ -136,8 +140,8 @@ namespace AyaNova.Api.Controllers
|
||||
//used to drive UI in case of unlicensed or attention required
|
||||
LicenseStatus = AyaNova.Core.License.ActiveKey.Status,
|
||||
MaintenanceExpired = AyaNova.Core.License.ActiveKey.MaintenanceExpired,
|
||||
ServerDbId = AyaNova.Core.License.ServerDbId,
|
||||
Company = AyaNova.Core.License.ActiveKey.RegisteredTo,
|
||||
ServerDbId = staff ? AyaNova.Core.License.ServerDbId : string.Empty,
|
||||
Company = staff ? AyaNova.Core.License.ActiveKey.RegisteredTo : string.Empty,
|
||||
SBuild = IsSubscriptionBuild,
|
||||
//used to drive UI in case of out of date release message to be shown to staff users non subscription only
|
||||
ShowUpdateAvailable = (Util.AyaNovaVersion.VersionString != AyaNova.Util.ServerGlobalOpsSettingsCache.LATEST_VERSION && !IsSubscriptionBuild),
|
||||
|
||||
@@ -79,9 +79,15 @@ namespace AyaNova.Api.Controllers
|
||||
GlobalOpsBackupSettingsBiz biz = GlobalOpsBackupSettingsBiz.GetBiz(ct, HttpContext);
|
||||
if (!Authorized.HasModifyRole(HttpContext.Items, biz.BizType))
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
var o = await biz.PutAsync(updatedObject);
|
||||
var o = await biz.PutAsync(updatedObject);
|
||||
if (o == null)
|
||||
return StatusCode(409, new ApiErrorResponse(biz.Errors));
|
||||
{
|
||||
//case 4763: the settings can now fail validation, which is not a concurrency conflict
|
||||
if (biz.Errors.Exists(z => z.Code == ApiErrorCode.CONCURRENCY_CONFLICT))
|
||||
return StatusCode(409, new ApiErrorResponse(biz.Errors));
|
||||
else
|
||||
return BadRequest(new ApiErrorResponse(biz.Errors));
|
||||
}
|
||||
return Ok(ApiOkResponse.Response(new { Concurrency = o.Concurrency }));
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,10 @@ namespace AyaNova.Api.Controllers
|
||||
if (o == null)
|
||||
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
|
||||
|
||||
//case 4724: the SMTP password never goes to the client (detached first so the mask can't be saved)
|
||||
ct.Entry(o).State = EntityState.Detached;
|
||||
o.SmtpPassword = GlobalOpsNotificationSettingsBiz.SMTP_PASSWORD_MASK;
|
||||
|
||||
return Ok(ApiOkResponse.Response(o));
|
||||
}
|
||||
|
||||
|
||||
@@ -69,9 +69,10 @@ namespace AyaNova.Api.Controllers
|
||||
//stream the file contents into a json object and return
|
||||
|
||||
//build the full path from the log file name and defined path
|
||||
var logFilePath = System.IO.Path.Combine(ServerBootConfig.AYANOVA_LOG_PATH, logname);
|
||||
//case 4715: only a bare file name inside the log folder
|
||||
var logFilePath = FileUtil.FullPathInFolderOrNull(ServerBootConfig.AYANOVA_LOG_PATH, logname);
|
||||
//does file exist?
|
||||
if (!System.IO.File.Exists(logFilePath))
|
||||
if (logFilePath == null || !System.IO.File.Exists(logFilePath))
|
||||
{
|
||||
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
|
||||
}
|
||||
@@ -117,7 +118,8 @@ namespace AyaNova.Api.Controllers
|
||||
//Iterate all log files and build return
|
||||
var files = System.IO.Directory.GetFiles(ServerBootConfig.AYANOVA_LOG_PATH, "log-ayanova*.txt");
|
||||
|
||||
var ret = files.Where(z => !z.EndsWith("ayanova.txt")).OrderByDescending(z => z).Select(z => System.IO.Path.GetFileName(z)).ToList();
|
||||
//newest archive first by file date, archive numbers only increase since NLog 6 so name order breaks past 9
|
||||
var ret = files.Where(z => !z.EndsWith("ayanova.txt")).OrderByDescending(z => System.IO.File.GetLastWriteTimeUtc(z)).Select(z => System.IO.Path.GetFileName(z)).ToList();
|
||||
ret.Insert(0, "log-ayanova.txt");
|
||||
return Ok(ApiOkResponse.Response(ret));
|
||||
}
|
||||
@@ -148,8 +150,9 @@ namespace AyaNova.Api.Controllers
|
||||
if (!Authorized.HasReadFullRole(user.Roles, AyaType.LogFile))
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
|
||||
var logFilePath = System.IO.Path.Combine(ServerBootConfig.AYANOVA_LOG_PATH, logname);
|
||||
if (!System.IO.File.Exists(logFilePath))
|
||||
//case 4715: only a bare file name inside the log folder
|
||||
var logFilePath = FileUtil.FullPathInFolderOrNull(ServerBootConfig.AYANOVA_LOG_PATH, logname);
|
||||
if (logFilePath == null || !System.IO.File.Exists(logFilePath))
|
||||
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
|
||||
|
||||
//Note: this works with new Nlog file locking for write so it must be opening it as read and shareable
|
||||
|
||||
@@ -82,21 +82,49 @@ namespace AyaNova.Api.Controllers
|
||||
case "small":
|
||||
if (logo.Small == null)
|
||||
return NotFound();
|
||||
return new FileStreamResult(new MemoryStream(logo.Small), Microsoft.Net.Http.Headers.MediaTypeHeaderValue.Parse(logo.SmallType));
|
||||
return LogoFile(logo.Small);
|
||||
|
||||
case "medium":
|
||||
if (logo.Medium == null)
|
||||
return NotFound();
|
||||
return new FileStreamResult(new MemoryStream(logo.Medium), Microsoft.Net.Http.Headers.MediaTypeHeaderValue.Parse(logo.MediumType));
|
||||
return LogoFile(logo.Medium);
|
||||
|
||||
case "large":
|
||||
if (logo.Large == null)
|
||||
return NotFound();
|
||||
return new FileStreamResult(new MemoryStream(logo.Large), Microsoft.Net.Http.Headers.MediaTypeHeaderValue.Parse(logo.LargeType));
|
||||
return LogoFile(logo.Large);
|
||||
}
|
||||
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
|
||||
}
|
||||
|
||||
//case 4727 (F23): the type comes from the bytes, never from what was sent or stored, and the response may not run
|
||||
//script: an SVG opened directly by its URL is shown, its script is not run (an <img> tag is not affected)
|
||||
private FileStreamResult LogoFile(byte[] bytes)
|
||||
{
|
||||
Response.Headers["X-Content-Type-Options"] = "nosniff";
|
||||
Response.Headers["Content-Security-Policy"] = "default-src 'none'; style-src 'unsafe-inline'; sandbox";
|
||||
return new FileStreamResult(new MemoryStream(bytes), ImageTypeOrNull(bytes) ?? "application/octet-stream");
|
||||
}
|
||||
|
||||
//case 4727 (F23): PNG, JPEG, GIF, WebP or SVG, recognised by their content; null for anything else
|
||||
internal static string ImageTypeOrNull(byte[] b)
|
||||
{
|
||||
if (b == null || b.Length < 12)
|
||||
return null;
|
||||
if (b[0] == 0x89 && b[1] == 0x50 && b[2] == 0x4E && b[3] == 0x47 && b[4] == 0x0D && b[5] == 0x0A && b[6] == 0x1A && b[7] == 0x0A)
|
||||
return "image/png";
|
||||
if (b[0] == 0xFF && b[1] == 0xD8 && b[2] == 0xFF)
|
||||
return "image/jpeg";
|
||||
if (b[0] == 'G' && b[1] == 'I' && b[2] == 'F' && b[3] == '8' && (b[4] == '7' || b[4] == '9') && b[5] == 'a')
|
||||
return "image/gif";
|
||||
if (b[0] == 'R' && b[1] == 'I' && b[2] == 'F' && b[3] == 'F' && b[8] == 'W' && b[9] == 'E' && b[10] == 'B' && b[11] == 'P')
|
||||
return "image/webp";
|
||||
var head = System.Text.Encoding.UTF8.GetString(b, 0, System.Math.Min(b.Length, 4096)).TrimStart((char)0xFEFF, ' ', '\t', '\r', '\n');
|
||||
if (head.StartsWith("<") && head.Contains("<svg", System.StringComparison.OrdinalIgnoreCase))
|
||||
return "image/svg+xml";
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
@@ -118,7 +146,8 @@ namespace AyaNova.Api.Controllers
|
||||
if (!serverState.IsOpen)
|
||||
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
|
||||
|
||||
if (!Authorized.HasReadFullRole(HttpContext.Items, AyaType.Global))
|
||||
//case 4727 (F23): change rights (was read rights, which BizAdminRestricted has)
|
||||
if (!Authorized.HasModifyRole(HttpContext.Items, AyaType.Global))
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
|
||||
if (string.IsNullOrWhiteSpace(size))
|
||||
@@ -145,19 +174,22 @@ namespace AyaNova.Api.Controllers
|
||||
await file.CopyToAsync(memoryStream);
|
||||
if (memoryStream.Length > ServerBootConfig.MAX_LOGO_UPLOAD_BYTES)
|
||||
return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_LENGTH_EXCEEDED, null, $"Logo files must be smaller than {ServerBootConfig.MAX_LOGO_UPLOAD_BYTES} maximum"));
|
||||
var imageType = ImageTypeOrNull(memoryStream.ToArray());
|
||||
if (imageType == null)
|
||||
return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_INVALID_VALUE, null, "Logo files must be PNG, JPEG, GIF, WebP or SVG images"));
|
||||
switch (size)
|
||||
{
|
||||
case "small":
|
||||
logo.Small = memoryStream.ToArray();
|
||||
logo.SmallType = file.ContentType;
|
||||
logo.SmallType = imageType;
|
||||
break;
|
||||
case "medium":
|
||||
logo.Medium = memoryStream.ToArray();
|
||||
logo.MediumType = file.ContentType;
|
||||
logo.MediumType = imageType;
|
||||
break;
|
||||
case "large":
|
||||
logo.Large = memoryStream.ToArray();
|
||||
logo.LargeType = file.ContentType;
|
||||
logo.LargeType = imageType;
|
||||
break;
|
||||
|
||||
}
|
||||
@@ -179,7 +211,8 @@ namespace AyaNova.Api.Controllers
|
||||
if (!serverState.IsOpen)
|
||||
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
|
||||
|
||||
if (!Authorized.HasReadFullRole(HttpContext.Items, AyaType.Global))
|
||||
//case 4727 (F23): change rights (was read rights, which BizAdminRestricted has)
|
||||
if (!Authorized.HasModifyRole(HttpContext.Items, AyaType.Global))
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
|
||||
if (string.IsNullOrWhiteSpace(size))
|
||||
|
||||
@@ -51,7 +51,8 @@ namespace AyaNova.Api.Controllers
|
||||
if (AyaNova.Core.License.ActiveKey.Status == AyaNova.Core.License.AyaNovaLicenseKey.LicenseStatus.ActiveTrial)
|
||||
showSampleLogins = await AyaNova.Util.DbUtil.DBHasTrialUsersAsync(ct, log);
|
||||
|
||||
bool suIsDefault = await UserBiz.SuperIsDefaultCredsAsync(ct);
|
||||
//case 4725: the default SuperUser login is offered only while unlicensed or in trial
|
||||
bool suIsDefault = !AyaNova.Core.License.PurchasedLicense() && await UserBiz.SuperIsDefaultCredsAsync(ct);
|
||||
//confirm if there are logo's to show as well
|
||||
var logo = await ct.Logo.AsNoTracking().SingleOrDefaultAsync();
|
||||
|
||||
@@ -204,6 +205,10 @@ namespace AyaNova.Api.Controllers
|
||||
if (!ModelState.IsValid)
|
||||
return BadRequest(new ApiErrorResponse(ModelState));
|
||||
|
||||
//case 4723: staff only, as in the client (no customer contacts or subcontractors)
|
||||
var userType = UserTypeFromContext.Type(HttpContext.Items);
|
||||
if (userType == UserType.Customer || userType == UserType.HeadOffice || userType == UserType.ServiceContractor)
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
|
||||
foreach (long l in notifyDirectMessage.Users)
|
||||
{
|
||||
@@ -247,11 +252,13 @@ namespace AyaNova.Api.Controllers
|
||||
if (!ModelState.IsValid)
|
||||
return BadRequest(new ApiErrorResponse(ModelState));
|
||||
|
||||
if (!ServerGlobalOpsSettingsCache.Notify.SmtpDeliveryActive)
|
||||
return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_MISSING_PROPERTY, null, "Email notifications are set to OFF at server, unable to send 'on request' type SMTP notification"));
|
||||
|
||||
//for now I'm allowing any authenticated user to call this route intentionally as the use case is for our own internal messaging from the Customer form only or for API users who want to send
|
||||
//email when rendering a report (case 4310).
|
||||
//Staff users may send to any address: the use case is our own messaging from the Customer form etc and
|
||||
//API users and report scripts that send email when rendering a report (case 4310).
|
||||
//case 4723: customer contacts may not use this route at all; an address taken from a record
|
||||
//needs read rights on that record
|
||||
var userType = UserTypeFromContext.Type(HttpContext.Items);
|
||||
if (userType == UserType.Customer || userType == UserType.HeadOffice)
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
|
||||
//validate incoming data
|
||||
if (string.IsNullOrWhiteSpace(notifyDirectSMTP.Subject))
|
||||
@@ -275,6 +282,8 @@ namespace AyaNova.Api.Controllers
|
||||
return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_MISSING_PROPERTY, "ToAddress", "No address or object type and id specified no where to send this"));
|
||||
case AyaType.Customer:
|
||||
{
|
||||
if (!Authorized.HasReadFullRole(HttpContext.Items, AyaType.Customer))
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
var o = await ct.Customer.AsNoTracking().Where(x => x.Id == notifyDirectSMTP.ObjectId).Select(x => new { x.Name, x.EmailAddress, x.Active }).FirstOrDefaultAsync();
|
||||
if (o == null)
|
||||
{
|
||||
@@ -289,6 +298,8 @@ namespace AyaNova.Api.Controllers
|
||||
break;
|
||||
case AyaType.HeadOffice:
|
||||
{
|
||||
if (!Authorized.HasReadFullRole(HttpContext.Items, AyaType.HeadOffice))
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
var o = await ct.HeadOffice.AsNoTracking().Where(x => x.Id == notifyDirectSMTP.ObjectId).Select(x => new { x.Name, x.EmailAddress, x.Active }).FirstOrDefaultAsync();
|
||||
if (o == null)
|
||||
{
|
||||
@@ -303,6 +314,8 @@ namespace AyaNova.Api.Controllers
|
||||
break;
|
||||
case AyaType.Vendor:
|
||||
{
|
||||
if (!Authorized.HasReadFullRole(HttpContext.Items, AyaType.Vendor))
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
var o = await ct.Vendor.AsNoTracking().Where(x => x.Id == notifyDirectSMTP.ObjectId).Select(x => new { x.Name, x.EmailAddress, x.Active }).FirstOrDefaultAsync();
|
||||
if (o == null)
|
||||
{
|
||||
@@ -317,15 +330,20 @@ namespace AyaNova.Api.Controllers
|
||||
break;
|
||||
case AyaType.User:
|
||||
{
|
||||
var o = await ct.User.Include(z => z.UserOptions).AsNoTracking().Where(x => x.Id == notifyDirectSMTP.ObjectId).Select(x => new { x.Name, x.UserOptions.EmailAddress, x.Active }).FirstOrDefaultAsync();
|
||||
var o = await ct.User.Include(z => z.UserOptions).AsNoTracking().Where(x => x.Id == notifyDirectSMTP.ObjectId).Select(x => new { x.Name, x.UserOptions.EmailAddress, x.Active, x.UserType }).FirstOrDefaultAsync();
|
||||
if (o == null)
|
||||
{
|
||||
return BadRequest(new ApiErrorResponse(ApiErrorCode.NOT_FOUND, "ObjectId"));
|
||||
}
|
||||
//a customer contact's user record is read with Customer rights, as on the Contact form
|
||||
bool isContact = o.UserType == UserType.Customer || o.UserType == UserType.HeadOffice;
|
||||
if (!Authorized.HasReadFullRole(HttpContext.Items, AyaType.User) && !(isContact && Authorized.HasReadFullRole(HttpContext.Items, AyaType.Customer)))
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
//case 4762: these said "Vendor"
|
||||
if (string.IsNullOrWhiteSpace(o.EmailAddress))
|
||||
return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_MISSING_PROPERTY, "EmailAddress", $"Vendor {o.Name} doesn't have an email address no where to send this"));
|
||||
return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_MISSING_PROPERTY, "EmailAddress", $"User {o.Name} doesn't have an email address no where to send this"));
|
||||
if (o.Active == false)
|
||||
return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, "Active", $"Vendor {o.Name} is not active, only active Vendors can be emailed directly"));
|
||||
return BadRequest(new ApiErrorResponse(ApiErrorCode.INVALID_OPERATION, "Active", $"User {o.Name} is not active, only active users can be emailed directly"));
|
||||
notifyDirectSMTP.ToAddress = o.EmailAddress;
|
||||
}
|
||||
break;
|
||||
@@ -335,6 +353,10 @@ namespace AyaNova.Api.Controllers
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if (!ServerGlobalOpsSettingsCache.Notify.SmtpDeliveryActive)
|
||||
return BadRequest(new ApiErrorResponse(ApiErrorCode.VALIDATION_MISSING_PROPERTY, null, "Email notifications are set to OFF at server, unable to send 'on request' type SMTP notification"));
|
||||
|
||||
var UserId = UserIdFromContext.Id(HttpContext.Items);
|
||||
|
||||
IMailer m = AyaNova.Util.ServiceProviderProvider.Mailer;
|
||||
|
||||
@@ -75,6 +75,13 @@ namespace AyaNova.Api.Controllers
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
|
||||
|
||||
//case 4712: customer contacts get only the pick lists of the service request screen, limited to their own records
|
||||
var contactVariant = await ContactPickListVariantAsync(pickListParams.AyaType, pickListParams.ListVariant);
|
||||
if (contactVariant == CONTACT_DENIED)
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
if (contactVariant != null)
|
||||
pickListParams.ListVariant = contactVariant;
|
||||
|
||||
//Instantiate the business object handler
|
||||
PickListBiz biz = PickListBiz.GetBiz(ct, HttpContext);
|
||||
|
||||
@@ -122,6 +129,10 @@ namespace AyaNova.Api.Controllers
|
||||
if (!Authorized.HasSelectRole(HttpContext.Items, pickListSingleParams.AyaType))
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
|
||||
//case 4712: a customer contact gets names only of its own customers, their units, and unit models
|
||||
if (!await ContactMayNameRecordAsync(pickListSingleParams.AyaType, pickListSingleParams.Id))
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
|
||||
//Instantiate the business object handler
|
||||
PickListBiz biz = PickListBiz.GetBiz(ct, HttpContext);
|
||||
|
||||
@@ -276,9 +287,68 @@ namespace AyaNova.Api.Controllers
|
||||
}
|
||||
|
||||
|
||||
//case 4712: marker for a pick list a customer contact may not use
|
||||
private const string CONTACT_DENIED = "\u0000contact-denied";
|
||||
|
||||
//case 4712: for a customer contact, the variant that limits a pick list to its own records:
|
||||
//null when the caller is not a contact (request used as is), CONTACT_DENIED for any list the
|
||||
//customer screens do not use or a customer the contact does not own
|
||||
private async Task<string> ContactPickListVariantAsync(AyaType aType, string requestedVariant)
|
||||
{
|
||||
var userType = UserTypeFromContext.Type(HttpContext.Items);
|
||||
if (userType != UserType.Customer && userType != UserType.HeadOffice)
|
||||
return null;
|
||||
var userId = UserIdFromContext.Id(HttpContext.Items);
|
||||
var u = await ct.User.AsNoTracking().Where(x => x.Id == userId).Select(x => new { x.CustomerId, x.HeadOfficeId }).SingleOrDefaultAsync();
|
||||
long ownCustomerId = u?.CustomerId ?? 0;
|
||||
long ownHeadOfficeId = u?.HeadOfficeId ?? 0;
|
||||
switch (aType)
|
||||
{
|
||||
case AyaType.Customer:
|
||||
if (userType == UserType.Customer)
|
||||
return ownCustomerId == 0 ? CONTACT_DENIED : $"{ownCustomerId},{(int)AyaType.Customer}";
|
||||
return ownHeadOfficeId == 0 ? CONTACT_DENIED : $"{ownHeadOfficeId},{(int)AyaType.HeadOffice}";
|
||||
case AyaType.Unit:
|
||||
if (userType == UserType.Customer)
|
||||
return ownCustomerId == 0 ? CONTACT_DENIED : $"customerid:{ownCustomerId}";
|
||||
//head office contact: units of the customer chosen on the form, which must be one of its customers;
|
||||
//none until a customer is chosen
|
||||
var v = (requestedVariant ?? string.Empty).Split(':');
|
||||
long requestedCustomerId = 0;
|
||||
if (v.Length == 2 && v[0].Trim() == "customerid")
|
||||
long.TryParse(v[1].Trim(), System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out requestedCustomerId);
|
||||
if (requestedCustomerId == 0)
|
||||
return "customerid:-1";
|
||||
return await UserBiz.CustomerContactOwnsCustomerAsync(ct, userId, requestedCustomerId) ? $"customerid:{requestedCustomerId}" : CONTACT_DENIED;
|
||||
case AyaType.UnitModel:
|
||||
return requestedVariant;//unit models are not customer data
|
||||
default:
|
||||
return CONTACT_DENIED;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//case 4712: may the caller see this record's pick list name (always true for staff)
|
||||
private async Task<bool> ContactMayNameRecordAsync(AyaType aType, long id)
|
||||
{
|
||||
var userType = UserTypeFromContext.Type(HttpContext.Items);
|
||||
if (userType != UserType.Customer && userType != UserType.HeadOffice)
|
||||
return true;
|
||||
var userId = UserIdFromContext.Id(HttpContext.Items);
|
||||
switch (aType)
|
||||
{
|
||||
case AyaType.Customer:
|
||||
return id == 0 || await UserBiz.CustomerContactOwnsCustomerAsync(ct, userId, id);
|
||||
case AyaType.Unit:
|
||||
if (id == 0)
|
||||
return true;
|
||||
var unitCustomerId = await ct.Unit.AsNoTracking().Where(x => x.Id == id).Select(x => (long?)x.CustomerId).FirstOrDefaultAsync();
|
||||
return unitCustomerId != null && await UserBiz.CustomerContactOwnsCustomerAsync(ct, userId, (long)unitCustomerId);
|
||||
case AyaType.UnitModel:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}//eoc
|
||||
}//ens
|
||||
@@ -76,6 +76,8 @@ namespace AyaNova.Api.Controllers
|
||||
if (!ModelState.IsValid)
|
||||
return BadRequest(new ApiErrorResponse(ModelState));
|
||||
var o = await biz.GetAsync(id);
|
||||
if (o == null && !biz.HasErrors)
|
||||
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
|
||||
if (o == null)
|
||||
return BadRequest(new ApiErrorResponse(biz.Errors));
|
||||
return Ok(ApiOkResponse.Response(o));
|
||||
@@ -145,6 +147,8 @@ namespace AyaNova.Api.Controllers
|
||||
if (!ModelState.IsValid)
|
||||
return BadRequest(new ApiErrorResponse(ModelState));
|
||||
var o = await biz.GetAsync(id);
|
||||
if (o == null && !biz.HasErrors)
|
||||
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
|
||||
if (o == null)
|
||||
return BadRequest(new ApiErrorResponse(biz.Errors));
|
||||
return Ok(ApiOkResponse.Response(new
|
||||
|
||||
@@ -11,6 +11,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using AyaNova.Models;
|
||||
using AyaNova.Api.ControllerHelpers;
|
||||
using AyaNova.Biz;
|
||||
using EnumsNET;
|
||||
using AyaNova.Util;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@@ -213,7 +214,9 @@ namespace AyaNova.Api.Controllers
|
||||
return BadRequest(new ApiErrorResponse(ModelState));
|
||||
var httpConnectionFeature = HttpContext.Features.Get<IHttpConnectionFeature>();
|
||||
var API_URL = $"http://127.0.0.1:{httpConnectionFeature.LocalPort}/api/{AyaNovaVersion.CurrentApiVersion}/";
|
||||
var result = await biz.RequestRenderReport(reportRequest, DateTime.UtcNow.AddMinutes(ServerBootConfig.AYANOVA_REPORT_RENDERING_TIMEOUT), API_URL, UserNameFromContext.Name(HttpContext.Items));
|
||||
//case 4632: inside IIS the report page's API calls are relayed to where this request arrived
|
||||
var relayTarget = ServerBootConfig.HOSTED_IN_IIS ? ReportApiRelay.FromRequest(HttpContext) : null;
|
||||
var result = await biz.RequestRenderReport(reportRequest, DateTime.UtcNow.AddMinutes(ServerBootConfig.AYANOVA_REPORT_RENDERING_TIMEOUT), API_URL, UserNameFromContext.Name(HttpContext.Items), relayTarget);
|
||||
if (result == null)
|
||||
return BadRequest(new ApiErrorResponse(biz.Errors));
|
||||
else
|
||||
@@ -288,7 +291,8 @@ namespace AyaNova.Api.Controllers
|
||||
if (!ModelState.IsValid)
|
||||
return BadRequest(new ApiErrorResponse(ModelState));
|
||||
|
||||
if (await UserBiz.ValidateDownloadTokenAndReturnUserAsync(t, ct) == null)
|
||||
var DownloadUser = await UserBiz.ValidateDownloadTokenAndReturnUserAsync(t, ct);
|
||||
if (DownloadUser == null)
|
||||
{
|
||||
await Task.Delay(ServerBootConfig.FAILED_AUTH_DELAY);//DOS protection
|
||||
return StatusCode(401, new ApiErrorResponse(ApiErrorCode.AUTHENTICATION_FAILED));
|
||||
@@ -300,6 +304,10 @@ namespace AyaNova.Api.Controllers
|
||||
{
|
||||
return NotFound(new ApiErrorResponse(ApiErrorCode.NOT_FOUND));
|
||||
}
|
||||
|
||||
//case 4727 (F22): only a template the user may use (its Roles) or may edit
|
||||
if (!DownloadUser.Roles.HasAnyFlags(o.Roles) && !Authorized.HasModifyRole(DownloadUser.Roles, AyaType.Report))
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
var asText = Newtonsoft.Json.JsonConvert.SerializeObject(
|
||||
o,
|
||||
Newtonsoft.Json.Formatting.None,
|
||||
@@ -329,6 +337,9 @@ namespace AyaNova.Api.Controllers
|
||||
if (!serverState.IsOpen)
|
||||
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
|
||||
|
||||
//case 4718: importing a template creates one, so it needs the same right as creating one (checked before the upload is read)
|
||||
if (!Authorized.HasCreateRole(HttpContext.Items, AyaType.Report))
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
|
||||
// AyaTypeId attachToObject = null;
|
||||
ApiUploadProcessor.ApiUploadedFilesResult uploadFormData = null;
|
||||
|
||||
@@ -60,6 +60,8 @@ namespace AyaNova.Api.Controllers
|
||||
{
|
||||
if (!serverState.IsOpen)
|
||||
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
|
||||
if (!CanSeeServiceSchedule())
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
if (!ModelState.IsValid)
|
||||
return BadRequest(new ApiErrorResponse(ModelState));
|
||||
|
||||
@@ -152,6 +154,8 @@ namespace AyaNova.Api.Controllers
|
||||
{
|
||||
if (!serverState.IsOpen)
|
||||
return StatusCode(503, new ApiErrorResponse(serverState.ApiErrorCode, null, serverState.Reason));
|
||||
if (!CanSeeServiceSchedule())
|
||||
return StatusCode(403, new ApiNotAuthorizedResponse());
|
||||
if (!ModelState.IsValid)
|
||||
return BadRequest(new ApiErrorResponse(ModelState));
|
||||
|
||||
@@ -460,6 +464,15 @@ namespace AyaNova.Api.Controllers
|
||||
return v.Serial.ToString() + " " + v.CustomerName;
|
||||
}
|
||||
|
||||
//case 4712: the service schedule (and its per user drill down) needs the same rights the client
|
||||
//uses to show the Schedule menu: open work orders, quotes or PMs
|
||||
private bool CanSeeServiceSchedule()
|
||||
{
|
||||
return Authorized.HasReadFullRole(HttpContext.Items, AyaType.WorkOrder)
|
||||
|| Authorized.HasReadFullRole(HttpContext.Items, AyaType.Quote)
|
||||
|| Authorized.HasReadFullRole(HttpContext.Items, AyaType.PM);
|
||||
}
|
||||
|
||||
|
||||
public enum ScheduleWorkOrderColorSource : int
|
||||
{
|
||||
|
||||
@@ -14,7 +14,6 @@ using Newtonsoft.Json.Linq;
|
||||
using AyaNova.Models;
|
||||
using AyaNova.Api.ControllerHelpers;
|
||||
using AyaNova.Biz;
|
||||
//using StackExchange.Profiling;
|
||||
|
||||
namespace AyaNova.Api.Controllers
|
||||
{
|
||||
|
||||
@@ -193,6 +193,8 @@ namespace AyaNova.Api.Controllers
|
||||
AYANOVA_LOG_PATH = ServerBootConfig.AYANOVA_LOG_PATH,
|
||||
AYANOVA_LOG_LEVEL = ServerBootConfig.AYANOVA_LOG_LEVEL,
|
||||
AYANOVA_LOG_ENABLE_LOGGER_DIAGNOSTIC_LOG = ServerBootConfig.AYANOVA_LOG_ENABLE_LOGGER_DIAGNOSTIC_LOG,
|
||||
AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE = ServerBootConfig.AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE,
|
||||
AYANOVA_TRUSTED_PROXIES = ServerBootConfig.AYANOVA_TRUSTED_PROXIES,
|
||||
serverinfo = ServerBootConfig.BOOT_DIAGNOSTIC_INFO,
|
||||
dbserverinfo = ServerBootConfig.DBSERVER_DIAGNOSTIC_INFO
|
||||
}));
|
||||
@@ -315,6 +317,8 @@ namespace AyaNova.Api.Controllers
|
||||
sbRet.AppendLine($"AYANOVA_LOG_PATH: {ServerBootConfig.AYANOVA_LOG_PATH}");
|
||||
sbRet.AppendLine($"AYANOVA_LOG_LEVEL: {ServerBootConfig.AYANOVA_LOG_LEVEL}");
|
||||
sbRet.AppendLine($"AYANOVA_LOG_ENABLE_LOGGER_DIAGNOSTIC_LOG: {ServerBootConfig.AYANOVA_LOG_ENABLE_LOGGER_DIAGNOSTIC_LOG}");
|
||||
sbRet.AppendLine($"AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE: {ServerBootConfig.AYANOVA_SMTP_ACCEPT_ANY_CERTIFICATE}");
|
||||
sbRet.AppendLine($"AYANOVA_TRUSTED_PROXIES: {ServerBootConfig.AYANOVA_TRUSTED_PROXIES}");
|
||||
|
||||
sbRet.AppendLine("#########################################################");
|
||||
sbRet.AppendLine("SERVER DIAGNOSTICS");
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user