Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
6d8b20b
FC2-COM-001: add SQL-backed site line and machine inventory store
abilgaiyan Nov 11, 2026
6d7c0b9
FC2-COM-001: keep registration store internal until transactional aut…
abilgaiyan Nov 11, 2026
ace2010
FC2-COM-001: add authenticated read-only machine directory screen
abilgaiyan Nov 11, 2026
ccdee06
FC2-COM-001: register machine administration route
abilgaiyan Nov 11, 2026
216e384
FC2-COM-001: expose authorized machine directory in dashboard navigation
abilgaiyan Nov 11, 2026
74969ec
FC2-COM-001: link authorized directory entries to operator evidence
abilgaiyan Nov 11, 2026
1d013de
FC2-COM-001: preselect authorized machine when opening evidence from …
abilgaiyan Nov 11, 2026
d27382d
Fix CS8057 caused by escaped newline in registration store comment
abilgaiyan Nov 11, 2026
6ef535e
FC2-COM-001: present existing Keycloak sign-in as FactoryConnect sign-in
abilgaiyan Nov 11, 2026
1a3fe54
Fix browser acceptance login selector after FactoryConnect sign-in la…
abilgaiyan Nov 11, 2026
5008abc
Align activity browser acceptance login selector with FactoryConnect …
abilgaiyan Nov 11, 2026
80c3828
Improve machine administration sign-in and empty-state guidance
abilgaiyan Nov 11, 2026
6baaedc
Require caller-owned authorized SQL transaction for master-data regis…
abilgaiyan Nov 11, 2026
ff70e1b
Implement transaction-bound authorized and audited master-data regist…
abilgaiyan Nov 11, 2026
bab7528
Expose authenticated audited master-data registration API
abilgaiyan Nov 11, 2026
c6b00be
Register master-data administration endpoint in API host
abilgaiyan Nov 11, 2026
0021b07
Add authenticated site line and machine registration form
abilgaiyan Nov 11, 2026
4f246f8
Add permission-scoped administration site inventory read
abilgaiyan Nov 11, 2026
99256b0
Expose authorized site inventory for machine registration
abilgaiyan Nov 11, 2026
c49a4b1
Replace manual site GUID entry with active-site selection
abilgaiyan Nov 11, 2026
2b22e15
Provide complete authorized machine inventory separate from evidence …
abilgaiyan Nov 11, 2026
dd0d2a1
Expose authorized full machine inventory API
abilgaiyan Nov 11, 2026
4ad88d1
Show registered machines independently from evidence-authorized machi…
abilgaiyan Nov 11, 2026
7cc0e08
Add authorized production-line inventory for administration
abilgaiyan Nov 12, 2026
7219a9b
Expose permission-controlled production-line inventory endpoint
abilgaiyan Nov 12, 2026
5fb65e8
Show production-line inventory with site names in administration
abilgaiyan Nov 12, 2026
be678cd
Preserve registration operation IDs for safe network retries
abilgaiyan Nov 12, 2026
ab2bbc4
Show registered sites and site names throughout machine administration
abilgaiyan Nov 12, 2026
cd16f2a
Add site and status filters to registered machine inventory
abilgaiyan Nov 12, 2026
51ca254
Filter production lines by site and retired status
abilgaiyan Nov 12, 2026
c224509
Provide actionable registration conflict and authorization feedback
abilgaiyan Nov 12, 2026
fe9c42d
Export filtered machine inventory as CSV for commissioning review
abilgaiyan Nov 12, 2026
16ec8c4
Clarify registration versus unverified acquisition in machine inventory
abilgaiyan Nov 12, 2026
2c22d0e
Define MTConnect discovery using existing Edge Agent configuration
abilgaiyan Nov 12, 2026
63bfc62
Add bounded secure MTConnect probe device metadata parser
abilgaiyan Nov 12, 2026
1031773
Reject duplicate MTConnect device and data item identities during dis…
abilgaiyan Nov 12, 2026
c9620d7
Add read-only MTConnect Devices XML commissioning preview
abilgaiyan Nov 12, 2026
c8e3622
Align MTConnect XML preview validation with backend parser
abilgaiyan Nov 12, 2026
82a98ef
Connect MTConnect device preview to reviewed machine registration form
abilgaiyan Nov 12, 2026
ed90a4c
Enable selected MTConnect device identity export for commissioning re…
abilgaiyan Nov 12, 2026
0930ade
Show discovered MTConnect DataItem metadata for signal mapping review
abilgaiyan Nov 12, 2026
1d7a21a
Preview existing Edge MTConnect Agent URLs and machine identities for…
abilgaiyan Nov 12, 2026
90709c6
Compare MTConnect probe device identities with existing Edge configur…
abilgaiyan Nov 12, 2026
a256329
Fix TypeScript nullable match access in MTConnect Edge comparison
abilgaiyan Nov 12, 2026
de20176
Flag conflicting Edge device identities and reconcile configured Mach…
abilgaiyan Nov 12, 2026
7f54e8a
Fix dashboard TypeScript declaration order for machine identity recon…
abilgaiyan Nov 12, 2026
bf4e663
Add authenticated read-only MTConnect probe discovery from existing E…
abilgaiyan Nov 12, 2026
76eee32
Wire read-only MTConnect Agent discovery endpoint into API host
abilgaiyan Nov 12, 2026
08234a1
Connect machine administration to authenticated live MTConnect Agent …
abilgaiyan Nov 12, 2026
9133915
Qualify live MTConnect probe against registered machines and configur…
abilgaiyan Nov 12, 2026
e6c6524
Display live Agent DeviceKey reconciliation status in machine adminis…
abilgaiyan Nov 12, 2026
39d3c57
Load trusted Edge MTConnect machine addresses without importing unrel…
abilgaiyan Nov 12, 2026
3fccc90
Enable opt-in trusted local Edge configuration for MTConnect live dis…
abilgaiyan Nov 12, 2026
d01d7ee
Document local Edge configuration opt-in and live MTConnect discovery…
abilgaiyan Nov 12, 2026
721def5
Expose authenticated Edge machine commissioning readiness without Age…
abilgaiyan Nov 12, 2026
19e6ecc
Show live Edge-to-V2 registration readiness on Machine Administration…
abilgaiyan Nov 12, 2026
ad42c2f
Let administrators select Edge-configured machines directly for live …
abilgaiyan Nov 12, 2026
d0691d4
Reject malformed Edge configuration field types with clear startup va…
abilgaiyan Nov 12, 2026
79f4afe
Add live-probe commissioning review with device and same-site product…
abilgaiyan Nov 12, 2026
c9522ff
Fix optional matched device ID in commissioning review TypeScript
abilgaiyan Nov 12, 2026
2cab62d
Allow preserving an existing Edge machine ID during registration
abilgaiyan Nov 12, 2026
af6962b
Audit explicit machine identity in master registration operation
abilgaiyan Nov 12, 2026
d3b737a
Accept optional existing Edge machine ID in admin registration API
abilgaiyan Nov 12, 2026
616a121
Register Edge-configured machines in V2 using their existing machine IDs
abilgaiyan Nov 12, 2026
b2c7384
Prepare one-click registration for unregistered Edge-configured machines
abilgaiyan Nov 12, 2026
2e1f7cd
Expose duplicate Edge machine identities and return actionable discov…
abilgaiyan Nov 12, 2026
34acd9e
Flag duplicate Edge MachineIds and disable unsafe commissioning actions
abilgaiyan Nov 12, 2026
3909bec
Align configured Edge inventory response type with duplicate identity…
abilgaiyan Nov 12, 2026
6f9fe81
Reject duplicate imported Edge machine identities within registration…
abilgaiyan Nov 12, 2026
e3aceaf
Add SQL-backed machine commissioning assignment with audited operatio…
abilgaiyan Nov 12, 2026
5c35f8f
Avoid treating MTConnect device IDs as globally unique across agents
abilgaiyan Nov 12, 2026
52a5b91
Expose authorized SQL read model for saved commissioning assignments
abilgaiyan Nov 12, 2026
9001719
Add authenticated API to list persisted machine commissioning assignm…
abilgaiyan Nov 12, 2026
03770f7
Implement authorized audited idempotent SQL commissioning assignment …
abilgaiyan Nov 12, 2026
7f0d6de
Add authenticated commissioning assignment save API
abilgaiyan Nov 12, 2026
bc97005
Let administrators save selected device and production line assignmen…
abilgaiyan Nov 12, 2026
8cc6a1c
Display saved commissioning assignments and require verified Agent id…
abilgaiyan Nov 12, 2026
e901b6c
Fix dashboard TypeScript declaration order for commissioning assignme…
abilgaiyan Nov 12, 2026
acfa7fe
Give authorized machine search its own accessible input identifier
abilgaiyan Nov 12, 2026
2eefe47
Clear stale Agent discovery before commissioning a different machine
abilgaiyan Nov 12, 2026
0abb268
Scope commissioning retry operation identity to authenticated session
abilgaiyan Nov 12, 2026
969a41c
Verify saved commissioning assignment by immediate authenticated SQL …
abilgaiyan Nov 12, 2026
49dfb7b
Distinguish confirmed commissioning save from read-back parsing failure
abilgaiyan Nov 12, 2026
1fd1f17
Document focused local SQL commissioning save/read acceptance procedure
abilgaiyan Nov 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions backend/FactoryConnect.Api/EdgeMtConnectConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.Text.Json;
using Microsoft.Extensions.Configuration;

namespace FactoryConnect.Api;

/// <summary>
/// Imports only MTConnect machine addresses from a trusted local Edge configuration file.
/// Never merges the Edge document's security, database or identity settings into the API.
/// </summary>
internal static class EdgeMtConnectConfiguration
{
internal static void Load(WebApplicationBuilder builder)
{
var path = builder.Configuration["MTConnect:EdgeConfigurationFile"];
if (string.IsNullOrWhiteSpace(path)) return;
if (!Path.IsPathFullyQualified(path))
throw new InvalidOperationException("MTConnect:EdgeConfigurationFile must be an absolute path.");
using var file = File.OpenRead(path);
if (file.Length > 2_000_000)
throw new InvalidOperationException("Edge configuration exceeds the 2 MB limit.");
using var document = JsonDocument.Parse(file, new JsonDocumentOptions { MaxDepth = 32 });
if (!document.RootElement.TryGetProperty("MTConnect", out var mtconnect) ||
!mtconnect.TryGetProperty("Machines", out var machines) ||
machines.ValueKind != JsonValueKind.Array || machines.GetArrayLength() > 500)
throw new InvalidOperationException("Edge configuration must contain MTConnect.Machines.");
var values = new Dictionary<string, string?>();
var seen = new HashSet<Guid>();
var index = 0;
foreach (var machine in machines.EnumerateArray())
{
if (machine.ValueKind != JsonValueKind.Object ||
!machine.TryGetProperty("MachineId", out var idValue) ||
idValue.ValueKind != JsonValueKind.String ||
!Guid.TryParse(idValue.GetString(), out var machineId) ||
!seen.Add(machineId) ||
!machine.TryGetProperty("DeviceKey", out var keyValue) ||
keyValue.ValueKind != JsonValueKind.String ||
string.IsNullOrWhiteSpace(keyValue.GetString()) ||
!machine.TryGetProperty("BaseUri", out var uriValue) ||
uriValue.ValueKind != JsonValueKind.String ||
!Uri.TryCreate(uriValue.GetString(), UriKind.Absolute, out var uri) ||
uri.Scheme is not ("http" or "https") ||
!string.IsNullOrEmpty(uri.UserInfo) ||
!string.IsNullOrEmpty(uri.Query) ||
!string.IsNullOrEmpty(uri.Fragment))
throw new InvalidOperationException("Invalid or duplicate MTConnect machine entry in Edge configuration.");
var prefix = $"MTConnect:Machines:{index++}:";
values[prefix + "MachineId"] = machineId.ToString();
values[prefix + "DeviceKey"] = keyValue.GetString();
values[prefix + "BaseUri"] = uri.ToString();
}
builder.Configuration.AddInMemoryCollection(values);
}
}
4 changes: 4 additions & 0 deletions backend/FactoryConnect.Api/EvidenceApiHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public static WebApplication Build(string[] args, Action<WebApplicationBuilder>?
var builder = WebApplication.CreateBuilder(new WebApplicationOptions { Args = args,
ApplicationName = typeof(EvidenceApiHost).Assembly.FullName, WebRootPath = webRoot });
configure?.Invoke(builder);
// Opt-in: import only MTConnect machine entries from the trusted local Edge file.
EdgeMtConnectConfiguration.Load(builder);
builder.Services.AddOperatorLogin(builder.Configuration);
// Forwarding is opt-in and accepts only explicitly enumerated proxy IP addresses.
// An unset configuration keeps direct HTTPS behavior unchanged.
Expand Down Expand Up @@ -247,6 +249,8 @@ public static WebApplication Build(string[] args, Action<WebApplicationBuilder>?
}).RequireRateLimiting("security-boundary");

app.MapActivityHistory();
app.MapMasterRegistration();
app.MapMtConnectDiscovery();

app.MapMethods("/api/{**path}", ["GET", "POST"], NotReady);
app.MapFallbackToFile("index.html");
Expand Down
234 changes: 234 additions & 0 deletions backend/FactoryConnect.Api/MasterRegistrationApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
using System.Security.Cryptography;
using FactoryConnect.Sql.Security;

namespace FactoryConnect.Api;

public static class MasterRegistrationApi
{
private sealed record CommissioningRequest(Guid OperationId, Guid MachineId, Guid ProductionLineId, string DeviceId);
private sealed record RegistrationRequest(Guid OperationId, string Kind, Guid? SiteId, string DisplayName, Guid? MachineId = null);

public static void MapMasterRegistration(this WebApplication app)
{
app.MapGet("/api/admin/v2/sites", async (HttpContext http, IConfiguration config, CancellationToken ct) =>
{
http.Response.Headers.CacheControl = "no-store";
if (!http.Request.IsHttps) return Results.Problem(statusCode: 403, title: "HTTPS required");
if (!Guid.TryParse(config["Evidence:InstallationId"], out var installationId) ||
string.IsNullOrWhiteSpace(config.GetConnectionString("Evidence")))
return Results.Problem(statusCode: 503, title: "Master-data authority unavailable");
if (!http.Request.Headers.TryGetValue("Authorization", out var authorization) ||
authorization.Count != 1 || !authorization[0]!.StartsWith("Bearer ", StringComparison.Ordinal) ||
authorization[0]!.Length > 192) return Results.Unauthorized();
byte[] bearer;
try { bearer = Convert.FromBase64String(authorization[0]![7..]); }
catch (FormatException) { return Results.Unauthorized(); }
if (bearer.Length != 32)
{
CryptographicOperations.ZeroMemory(bearer);
return Results.Unauthorized();
}
var hash = SHA256.HashData(bearer);
CryptographicOperations.ZeroMemory(bearer);
try
{
var sites = await new SqlSecurityAuthority(config.GetConnectionString("Evidence")!, installationId)
.ReadAdministrationSitesAsync(hash, ct);
return Results.Ok(new { sites });
}
catch (SecurityDeniedException) { return Results.Unauthorized(); }
catch (OperationCanceledException) when (ct.IsCancellationRequested) { throw; }
catch (Exception ex) { return AuthorityDiagnostics.Unavailable(http, ex, "administration-sites", "Site inventory unavailable"); }
finally { CryptographicOperations.ZeroMemory(hash); }
}).RequireRateLimiting("security-boundary");

app.MapGet("/api/admin/v2/machines", async (HttpContext http, IConfiguration config, CancellationToken ct) =>
{
http.Response.Headers.CacheControl = "no-store";
if (!http.Request.IsHttps) return Results.Problem(statusCode: 403, title: "HTTPS required");
if (!Guid.TryParse(config["Evidence:InstallationId"], out var installationId) ||
string.IsNullOrWhiteSpace(config.GetConnectionString("Evidence")))
return Results.Problem(statusCode: 503, title: "Master-data authority unavailable");
if (!http.Request.Headers.TryGetValue("Authorization", out var authorization) ||
authorization.Count != 1 || !authorization[0]!.StartsWith("Bearer ", StringComparison.Ordinal) ||
authorization[0]!.Length > 192) return Results.Unauthorized();
byte[] bearer;
try { bearer = Convert.FromBase64String(authorization[0]![7..]); }
catch (FormatException) { return Results.Unauthorized(); }
if (bearer.Length != 32)
{
CryptographicOperations.ZeroMemory(bearer);
return Results.Unauthorized();
}
var hash = SHA256.HashData(bearer);
CryptographicOperations.ZeroMemory(bearer);
try
{
var machines = await new SqlSecurityAuthority(config.GetConnectionString("Evidence")!, installationId)
.ReadAdministrationMachinesAsync(hash, ct);
return Results.Ok(new { machines });
}
catch (SecurityDeniedException) { return Results.Unauthorized(); }
catch (OperationCanceledException) when (ct.IsCancellationRequested) { throw; }
catch (Exception ex) { return AuthorityDiagnostics.Unavailable(http, ex, "administration-machines", "Machine inventory unavailable"); }
finally { CryptographicOperations.ZeroMemory(hash); }
}).RequireRateLimiting("security-boundary");

app.MapGet("/api/admin/v2/commissioning-assignments", async (HttpContext http, IConfiguration config, CancellationToken ct) =>
{
http.Response.Headers.CacheControl = "no-store";
if (!http.Request.IsHttps) return Results.Problem(statusCode: 403, title: "HTTPS required");
if (!Guid.TryParse(config["Evidence:InstallationId"], out var installationId) ||
string.IsNullOrWhiteSpace(config.GetConnectionString("Evidence")))
return Results.Problem(statusCode: 503, title: "Master-data authority unavailable");
if (!http.Request.Headers.TryGetValue("Authorization", out var authorization) ||
authorization.Count != 1 || !authorization[0]!.StartsWith("Bearer ", StringComparison.Ordinal) ||
authorization[0]!.Length > 192) return Results.Unauthorized();
byte[] bearer;
try { bearer = Convert.FromBase64String(authorization[0]![7..]); }
catch (FormatException) { return Results.Unauthorized(); }
if (bearer.Length != 32)
{
CryptographicOperations.ZeroMemory(bearer);
return Results.Unauthorized();
}
var hash = SHA256.HashData(bearer);
CryptographicOperations.ZeroMemory(bearer);
try
{
var assignments = await new SqlSecurityAuthority(config.GetConnectionString("Evidence")!, installationId)
.ReadCommissioningAssignmentsAsync(hash, ct);
return Results.Ok(new { assignments });
}
catch (SecurityDeniedException) { return Results.Unauthorized(); }
catch (OperationCanceledException) when (ct.IsCancellationRequested) { throw; }
catch (Exception ex) { return AuthorityDiagnostics.Unavailable(http, ex, "commissioning-assignments", "Commissioning assignments unavailable"); }
finally { CryptographicOperations.ZeroMemory(hash); }
}).RequireRateLimiting("security-boundary");

app.MapGet("/api/admin/v2/lines", async (HttpContext http, IConfiguration config, CancellationToken ct) =>
{
http.Response.Headers.CacheControl = "no-store";
if (!http.Request.IsHttps) return Results.Problem(statusCode: 403, title: "HTTPS required");
if (!Guid.TryParse(config["Evidence:InstallationId"], out var installationId) ||
string.IsNullOrWhiteSpace(config.GetConnectionString("Evidence")))
return Results.Problem(statusCode: 503, title: "Master-data authority unavailable");
if (!http.Request.Headers.TryGetValue("Authorization", out var authorization) ||
authorization.Count != 1 || !authorization[0]!.StartsWith("Bearer ", StringComparison.Ordinal) ||
authorization[0]!.Length > 192) return Results.Unauthorized();
byte[] bearer;
try { bearer = Convert.FromBase64String(authorization[0]![7..]); }
catch (FormatException) { return Results.Unauthorized(); }
if (bearer.Length != 32)
{
CryptographicOperations.ZeroMemory(bearer);
return Results.Unauthorized();
}
var hash = SHA256.HashData(bearer);
CryptographicOperations.ZeroMemory(bearer);
try
{
var lines = await new SqlSecurityAuthority(config.GetConnectionString("Evidence")!, installationId)
.ReadAdministrationLinesAsync(hash, ct);
return Results.Ok(new { lines });
}
catch (SecurityDeniedException) { return Results.Unauthorized(); }
catch (OperationCanceledException) when (ct.IsCancellationRequested) { throw; }
catch (Exception ex) { return AuthorityDiagnostics.Unavailable(http, ex, "administration-lines", "Production-line inventory unavailable"); }
finally { CryptographicOperations.ZeroMemory(hash); }
}).RequireRateLimiting("security-boundary");

app.MapPost("/api/admin/v2/master-registration", async (
HttpContext http, IConfiguration config, CancellationToken ct) =>
{
http.Response.Headers.CacheControl = "no-store";
if (!http.Request.IsHttps)
return Results.Problem(statusCode: 403, title: "HTTPS required");
if (!Guid.TryParse(config["Evidence:InstallationId"], out var installationId) ||
string.IsNullOrWhiteSpace(config.GetConnectionString("Evidence")))
return Results.Problem(statusCode: 503, title: "Master-data authority unavailable");
if (!http.Request.Headers.TryGetValue("Authorization", out var authorization) ||
authorization.Count != 1 || !authorization[0]!.StartsWith("Bearer ", StringComparison.Ordinal) ||
authorization[0]!.Length > 192)
return Results.Unauthorized();
byte[] bearer;
try { bearer = Convert.FromBase64String(authorization[0]![7..]); }
catch (FormatException) { return Results.Unauthorized(); }
if (bearer.Length != 32)
{
CryptographicOperations.ZeroMemory(bearer);
return Results.Unauthorized();
}
var sessionHash = SHA256.HashData(bearer);
CryptographicOperations.ZeroMemory(bearer);
try
{
RegistrationRequest? request;
try { request = await http.Request.ReadFromJsonAsync<RegistrationRequest>(cancellationToken: ct); }
catch (System.Text.Json.JsonException) { return Results.BadRequest(new { error = "Invalid registration JSON" }); }
if (request is null || request.OperationId == Guid.Empty ||
request.Kind is not ("site" or "line" or "machine") ||
(request.Kind == "site" ? request.SiteId is not null :
request.SiteId is null || request.SiteId == Guid.Empty) ||
string.IsNullOrWhiteSpace(request.DisplayName) ||
request.DisplayName.Trim().Length > 200 ||
(request.MachineId.HasValue && (request.Kind != "machine" || request.MachineId == Guid.Empty)))
return Results.BadRequest(new { error = "Invalid registration request" });
var recordId = await new SqlSecurityAuthority(config.GetConnectionString("Evidence")!, installationId)
.RegisterMasterRecordAsync(sessionHash, request.OperationId, request.Kind,
request.SiteId, request.DisplayName, ct, request.MachineId);
return Results.Ok(new { recordId, request.OperationId, request.Kind });
}
catch (SecurityDeniedException) { return Results.Unauthorized(); }
catch (SecurityConflictException) { return Results.Conflict(new { error = "Operation ID conflict" }); }
catch (InvalidOperationException) { return Results.Conflict(new { error = "Registration conflicts with existing master data" }); }
catch (OperationCanceledException) when (ct.IsCancellationRequested) { throw; }
catch (Exception ex) { return AuthorityDiagnostics.Unavailable(http, ex, "master-registration", "Master-data authority unavailable"); }
finally { CryptographicOperations.ZeroMemory(sessionHash); }
}).RequireRateLimiting("security-boundary");
app.MapPost("/api/admin/v2/commissioning-assignments", async (
HttpContext http, IConfiguration config, CancellationToken ct) =>
{
http.Response.Headers.CacheControl = "no-store";
if (!http.Request.IsHttps)
return Results.Problem(statusCode: 403, title: "HTTPS required");
if (!Guid.TryParse(config["Evidence:InstallationId"], out var installationId) ||
string.IsNullOrWhiteSpace(config.GetConnectionString("Evidence")))
return Results.Problem(statusCode: 503, title: "Master-data authority unavailable");
if (!http.Request.Headers.TryGetValue("Authorization", out var authorization) ||
authorization.Count != 1 || !authorization[0]!.StartsWith("Bearer ", StringComparison.Ordinal) ||
authorization[0]!.Length > 192)
return Results.Unauthorized();
byte[] bearer;
try { bearer = Convert.FromBase64String(authorization[0]![7..]); }
catch (FormatException) { return Results.Unauthorized(); }
if (bearer.Length != 32)
{
CryptographicOperations.ZeroMemory(bearer);
return Results.Unauthorized();
}
var sessionHash = SHA256.HashData(bearer);
CryptographicOperations.ZeroMemory(bearer);
try
{
CommissioningRequest? request;
try { request = await http.Request.ReadFromJsonAsync<CommissioningRequest>(cancellationToken: ct); }
catch (System.Text.Json.JsonException) { return Results.BadRequest(new { error = "Invalid commissioning JSON" }); }
if (request is null || request.OperationId == Guid.Empty ||
request.MachineId == Guid.Empty || request.ProductionLineId == Guid.Empty ||
string.IsNullOrWhiteSpace(request.DeviceId) || request.DeviceId.Trim().Length > 200)
return Results.BadRequest(new { error = "Invalid commissioning assignment" });
await new SqlSecurityAuthority(config.GetConnectionString("Evidence")!, installationId)
.SaveCommissioningAssignmentAsync(sessionHash, request.OperationId,
request.MachineId, request.ProductionLineId, request.DeviceId, ct);
return Results.Ok(new { saved = true, request.OperationId, acquisitionActivated = false });
}
catch (SecurityDeniedException) { return Results.Unauthorized(); }
catch (SecurityConflictException) { return Results.Conflict(new { error = "Operation ID conflict" }); }
catch (InvalidOperationException) { return Results.Conflict(new { error = "Machine or production line invalid for commissioning" }); }
catch (OperationCanceledException) when (ct.IsCancellationRequested) { throw; }
catch (Exception ex) { return AuthorityDiagnostics.Unavailable(http, ex, "commissioning-save", "Commissioning authority unavailable"); }
finally { CryptographicOperations.ZeroMemory(sessionHash); }
}).RequireRateLimiting("security-boundary");
}
}
Loading
Loading