namespace DodoSSH.Domain;
///
/// A group of users who can share vaults.
///
///
/// The tables exist from the first migration although team features ship in M3. Adding them
/// later would mean altering to introduce a foreign key on a live table, and
/// the cost of carrying two unused tables is far lower than that.
///
public sealed class Team
{
/// Primary key.
public Guid Id { get; set; }
/// Display name.
public string Name { get; set; } = string.Empty;
/// URL-safe unique identifier.
public string Slug { get; set; } = string.Empty;
/// Optional description.
public string? Description { get; set; }
/// Who created it.
public Guid CreatedByUserId { get; set; }
/// Creation timestamp.
public DateTimeOffset CreatedAtUtc { get; set; }
/// Soft-delete marker.
public DateTimeOffset? DeletedAtUtc { get; set; }
/// Members.
public ICollection Memberships { get; } = [];
/// Invitations to addresses that have no account here yet.
public ICollection Invitations { get; } = [];
}
///
/// A user's membership of a team.
///
///
/// Revoked memberships are retained rather than deleted, so historic audit entries remain
/// resolvable to a person.
///
public sealed class TeamMembership
{
/// Primary key.
public Guid Id { get; set; }
/// The team.
public Guid TeamId { get; set; }
/// The team.
public Team? Team { get; set; }
/// The member.
public Guid UserId { get; set; }
/// The member.
public UserAccount? User { get; set; }
/// Role within the team.
public TeamRole Role { get; set; }
/// Membership state.
public MembershipStatus Status { get; set; }
/// Who invited them.
public Guid? InvitedByUserId { get; set; }
/// When the invitation was accepted.
public DateTimeOffset? JoinedAtUtc { get; set; }
/// Creation timestamp.
public DateTimeOffset CreatedAtUtc { get; set; }
/// Soft-delete marker.
public DateTimeOffset? DeletedAtUtc { get; set; }
}
///
/// A standing offer of membership to an email address that has no account here yet.
///
///
///
/// Its own table rather than a with
/// . A membership names an account —
/// team_membership.user_id is not nullable and carries a foreign key to
/// — so an invitee who has never signed in has nothing for that row to
/// point at. Widening that column would make the unique index on (team, user) meaningless, because
/// PostgreSQL counts every NULL as distinct, and would silently change what every
/// m.UserId == user.Id query in the server means.
///
///
/// There is no token. Nothing is sent, because this server has no outbound mail path; the row
/// is an instruction to the sign-in path rather than a secret somebody presents. That is why it is
/// keyed on the address and why the address has to be one the identity provider marks verified before
/// the claim is honoured — an unclaimable invitation is an inconvenience, but one claimable by
/// anybody who can assert an address is a way into the team.
///
///
/// Revoked and accepted rows are retained rather than deleted, as is and
/// for the same reason: the uniqueness that matters is among pending invitations, and the
/// history of who invited whom stays resolvable.
///
///
public sealed class TeamInvitation
{
/// Primary key.
public Guid Id { get; set; }
/// The team.
public Guid TeamId { get; set; }
/// The team.
public Team? Team { get; set; }
/// The address invited. Case-insensitive.
public string Email { get; set; } = string.Empty;
/// Role the membership will carry when it is claimed.
public TeamRole Role { get; set; }
/// Who issued it.
public Guid InvitedByUserId { get; set; }
/// Creation timestamp.
public DateTimeOffset CreatedAtUtc { get; set; }
/// When it stops being claimable.
public DateTimeOffset ExpiresAtUtc { get; set; }
/// When an account with this address signed in and took it up.
public DateTimeOffset? AcceptedAtUtc { get; set; }
/// Which account took it up.
public Guid? AcceptedByUserId { get; set; }
/// Revocation timestamp.
public DateTimeOffset? RevokedAtUtc { get; set; }
}