using System.Diagnostics.CodeAnalysis;
using System.Globalization;
namespace DodoSSH.Client.Domain;
/// How a connection ended.
public enum ConnectionOutcome
{
/// The session ran and then ended — by the user, by the remote, or by the process closing.
///
/// One value for all three, deliberately. From an auditor's side "this person had a shell on that machine
/// for eleven minutes" is the fact; which of the two ends hung up first is not something this client can
/// establish reliably — a tab close and a remote hangup both arrive as the pump finishing — and a field
/// that guessed would be worse than one that does not claim to know.
///
Closed = 0,
/// The connection was attempted and did not open.
Failed = 1,
/// The host key was not the pinned one, so the client refused before authenticating.
///
/// Its own outcome rather than a kind of , because it is the only one that means
/// something about the host rather than about the network or the credentials. A run of these on
/// one machine is the single most interesting thing a connection log can show.
///
Refused = 2,
}
/// What kind of session a log entry is about.
public enum ConnectionKind
{
/// An interactive terminal.
Terminal = 0,
/// An SFTP session for moving files.
///
/// Recorded separately and not hidden. Opening the file browser is a second login as far as the remote's
/// own auth.log is concerned, so a log of ours that quietly omitted it would disagree with the
/// host's — and the person comparing the two would be right to trust the host.
///
Sftp = 1,
}
///
/// One connection that was made, decrypted.
///
///
///
/// What is here, and what deliberately is not. The host's label, its item id, the address as dialled,
/// when it started, how long it lasted, how it ended, and which user on which device did it. An audit log
/// with no actor is not an audit log — the whole reason these sync is that an administrator will read them
/// once teams land — so the actor is recorded and the SSH username is not. The two are different questions:
/// "who in this organisation opened a shell" is what an audit answers, and "which account they logged in as"
/// is a detail of the host that the host's own logs already have.
///
///
/// Written once, at close. Every field is known by then, so an entry never needs a second write —
/// which is what keeps a synced log from needing a merge, an outbox row per update, or any way to collide
/// with itself. A connection that is still running is not in here at all; it is shown from the workspace's
/// live state, which is the only place that knows.
///
///
public sealed record ConnectionLogSecret : IVaultSecret
{
/// What the host was called at the time, or a plain address when nothing named it.
///
/// A copy rather than a lookup through , and that is the point of it: the bookmark
/// can be renamed or deleted, and a history that changed retroactively when somebody tidied their
/// keychain would be a history nobody could rely on.
///
public required string HostLabel { get; init; }
/// The address as dialled, user@host:port style, or whatever was typed.
public required string Address { get; init; }
/// The host item this was, or null when the connection did not come from one.
public Guid? HostId { get; init; }
/// Whether this was a terminal or a file-transfer session.
public ConnectionKind Kind { get; init; }
/// When it started.
public required DateTimeOffset StartedAt { get; init; }
/// How long it lasted.
///
/// A duration rather than an end time, because it is the thing anybody reads — and because the two clocks
/// involved are the same one, so storing both would be storing a value and its own arithmetic.
///
public TimeSpan Duration { get; init; }
/// How it ended.
public ConnectionOutcome Outcome { get; init; }
/// Which machine it was made from, as that machine calls itself.
public required string DeviceName { get; init; }
/// Which account in this organisation made it.
public Guid ActorUserId { get; init; }
/// What this entry is called, derived from what it records.
///
public string Label => string.Create(CultureInfo.InvariantCulture, $"{HostLabel} ({Address})");
/// Whether this is storable, and why not if it is not.
///
/// A negative duration is refused rather than clamped. It can only come from a payload written elsewhere
/// — nothing here can produce one — and a log that displayed "-3 hours" would leave a reader unable to
/// tell a corrupt entry from a clock they should worry about.
///
public bool TryValidate([NotNullWhen(false)] out string? reason)
{
if (string.IsNullOrWhiteSpace(HostLabel))
{
reason = "A connection log entry needs the host it was about.";
return false;
}
if (string.IsNullOrWhiteSpace(Address))
{
reason = "A connection log entry needs the address that was dialled.";
return false;
}
if (string.IsNullOrWhiteSpace(DeviceName))
{
reason = "A connection log entry needs the machine it was made from.";
return false;
}
if (Duration < TimeSpan.Zero)
{
reason = "A connection cannot have lasted a negative amount of time.";
return false;
}
if (HostId == Guid.Empty)
{
reason = "A host reference cannot be an empty id; use no host instead.";
return false;
}
reason = null;
return true;
}
}