< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Domain.Entities.Person
Assembly: ArturRios.Heimdall.Domain
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Domain/ArturRios.Heimdall.Domain/Entities/Person.cs
Line coverage
100%
Covered lines: 42
Uncovered lines: 0
Coverable lines: 42
Total lines: 304
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage
100%
Covered methods: 42
Fully covered methods: 42
Total methods: 42
Method coverage: 100%
Full method coverage: 100%

Metrics

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Domain/ArturRios.Heimdall.Domain/Entities/Person.cs

#LineLine coverage
 1using System.ComponentModel.DataAnnotations;
 2using ArturRios.Data.Relational.Core.Entities;
 3
 4namespace ArturRios.Heimdall.Domain.Entities;
 5
 6/// <summary>
 7///     A registered identity. Has no direct scope attribute — its relationship to scopes is
 8///     derived from its role: a <c>User</c> belongs to exactly one scope (via <c>SCOPE_USER</c>),
 9///     a <c>ScopeAdmin</c> owns one or more scopes (via <c>SCOPE_OWNER</c>), and a
 10///     <c>SystemAdmin</c> belongs to no scope.
 11/// </summary>
 12public class Person : Entity<long>
 13{
 14    /// <summary>
 15    ///     External identifier, generated on creation and used everywhere the person is addressed
 16    ///     from outside the database (API paths, response bodies, token claims). The internal
 17    ///     <see cref="Entity{TKey}.Id" /> is never exposed to callers.
 18    /// </summary>
 1243119    public Guid PublicId { get; set; } = Guid.NewGuid();
 20
 21    /// <summary>Full name. Required, max 200 characters.</summary>
 22    [MaxLength(200)]
 490223    public string Name { get; set; } = string.Empty;
 24
 25    /// <summary>
 26    ///     Email address. Unique within the scope for <c>User</c>s (via <c>SCOPE_USER</c>);
 27    ///     unique system-wide for <c>ScopeAdmin</c>s and <c>SystemAdmin</c>s.
 28    /// </summary>
 29    [MaxLength(256)]
 540430    public string Email { get; set; } = string.Empty;
 31
 32    /// <summary>Hash computed from the password and <see cref="Salt" />.</summary>
 374033    public byte[] PasswordHash { get; set; } = [];
 34
 35    /// <summary>Randomly generated per person, used to hash the password.</summary>
 372136    public byte[] Salt { get; set; } = [];
 37
 38    /// <summary>Logical deletion flag. Logically deleted persons are excluded from default queries.</summary>
 210139    public bool IsDeleted { get; set; }
 40
 41    /// <summary>
 42    ///     When the record was logically deleted, or <c>null</c> while it is live. The retention
 43    ///     window that ends in anonymisation is measured from here (NFR-20).
 44    /// </summary>
 45    /// <remarks>
 46    ///     <see cref="UpdatedAt" /> cannot serve: it moves on any later write, so a record touched
 47    ///     after its deletion would have its window silently restarted. This one is written once, by
 48    ///     the logical deletion, and never again.
 49    /// </remarks>
 36150    public DateTime? DeletedAt { get; set; }
 51
 52    /// <summary>
 53    ///     Why the record was deleted (see <c>DeletionKinds</c>), or <c>null</c> while it is live.
 54    ///     Decides which of the schedule's two deadlines applies.
 55    /// </summary>
 29856    public int? DeletionKind { get; set; }
 57
 58    /// <summary>
 59    ///     When the record was anonymised, or <c>null</c> if it has not been. Set by the scheduled
 60    ///     pass, and the reason a second pass over the same row is a no-op rather than a repeated
 61    ///     overwrite.
 62    /// </summary>
 75863    public DateTime? AnonymisedAt { get; set; }
 64
 65    /// <summary>
 66    ///     When the data subject asked to be erased (UC-42), or <c>null</c> if they have not. Set
 67    ///     once, by the request, and never moved: the statutory deadline runs from the request, not
 68    ///     from any later event.
 69    /// </summary>
 37270    public DateTime? ErasureRequestedAt { get; set; }
 71
 72    /// <summary>
 73    ///     The deadline the erasure must not outlive, computed when the request is made.
 74    /// </summary>
 75    /// <remarks>
 76    ///     Stored rather than derived on read, and that is the point. GDPR Art. 12(3) attaches an
 77    ///     obligation at the moment of the request; recomputing it from the current configuration
 78    ///     would let a later change to the deadline move an obligation that had already attached —
 79    ///     forward, which is unlawful, or backward, which would silently mark existing requests
 80    ///     overdue.
 81    /// </remarks>
 31882    public DateTime? ErasureDueAt { get; set; }
 83
 84    /// <summary>
 85    ///     Why the erasure could not be carried out yet, or <c>null</c> if nothing blocks it. One of
 86    ///     the application's own canonical messages, never caller input.
 87    /// </summary>
 88    /// <remarks>
 89    ///     A blocked request is still a request: the deadline keeps running, and this is what makes
 90    ///     the reason visible to the administrator who has to clear it, rather than the request
 91    ///     failing and leaving no trace.
 92    /// </remarks>
 93    [MaxLength(500)]
 30794    public string? ErasureBlockedReason { get; set; }
 95
 96    /// <summary>Email verification status.</summary>
 150297    public bool EmailVerified { get; set; }
 98
 99    /// <summary>
 100    ///     Consecutive failed password attempts since the last successful login. Reset to zero on
 101    ///     success, and the counter <see cref="LockedOutUntil" /> is derived from.
 102    /// </summary>
 744103    public int FailedLoginAttempts { get; set; }
 104
 105    /// <summary>
 106    ///     When set and still in the future, login is refused regardless of the password supplied —
 107    ///     the per-account half of the brute-force defence, alongside the per-IP request limiter. The
 108    ///     refusal is UC-11's ordinary <c>InvalidCredentials</c>, so a lockout is not observable to a
 109    ///     caller who does not already know the password.
 110    /// </summary>
 382111    public DateTime? LockedOutUntil { get; set; }
 112
 113    /// <summary>
 114    ///     Failed second-factor verifications at sign-in (UC-38) since the last successful one,
 115    ///     whatever challenge each was made against (FR-AU-12). A correct password does not reset it;
 116    ///     only a second factor that checks out does.
 117    /// </summary>
 118    /// <remarks>
 119    ///     This and the two columns after it are written only through <c>IAtomicWrites</c>, never by
 120    ///     a whole-row save: the mapping ignores them after insert, so a save of a copy read before a
 121    ///     failure was counted cannot write the count back down. The same holds for the three
 122    ///     re-authentication columns below.
 123    /// </remarks>
 681124    public int FailedSecondFactorAttempts { get; set; }
 125
 126    /// <summary>
 127    ///     When set and still in the future, every second factor presented at sign-in is refused,
 128    ///     right or wrong, with UC-38's ordinary refusal (FR-AU-12).
 129    /// </summary>
 344130    public DateTime? SecondFactorLockedOutUntil { get; set; }
 131
 132    /// <summary>
 133    ///     How many second-factor lockouts the account has had since its last successful second
 134    ///     factor. Each lockout lasts twice as long as the one before it, up to a cap (FR-AU-12).
 135    /// </summary>
 287136    public int SecondFactorLockouts { get; set; }
 137
 138    /// <summary>
 139    ///     Failed re-authentications — a password or a second factor checked under a bearer token, such
 140    ///     as disabling two-factor authentication or changing one's own email — in the window that
 141    ///     began at <see cref="ReauthenticationWindowStartedAt" /> (FR-AU-13). Kept apart from
 142    ///     <see cref="FailedLoginAttempts" />, so a stolen token cannot lock its owner out of signing in.
 143    /// </summary>
 470144    public int FailedReauthenticationAttempts { get; set; }
 145
 146    /// <summary>
 147    ///     When the first of the failures in <see cref="FailedReauthenticationAttempts" /> was counted, or
 148    ///     <c>null</c> when none is. A failure after the window has run out starts a new one.
 149    /// </summary>
 679150    public DateTime? ReauthenticationWindowStartedAt { get; set; }
 151
 152    /// <summary>
 153    ///     When set and still in the future, every re-authentication is refused, right or wrong, with
 154    ///     the same answer a wrong password or code gets (FR-AU-13).
 155    /// </summary>
 452156    public DateTime? ReauthenticationPausedUntil { get; set; }
 157
 158    /// <summary>
 159    ///     Account emails the person's own requests have had sent in the window ending at
 160    ///     <see cref="AccountEmailWindowEndsAt" /> (FR-PE-16): verification links they asked for again,
 161    ///     two-factor setup codes, and the notice an own email change sends to the previous address.
 162    ///     Written only by the targeted write that reserves a send, like the factor budgets.
 163    /// </summary>
 306164    public int AccountEmailsSent { get; set; }
 165
 166    /// <summary>
 167    ///     When the current one-day window of <see cref="AccountEmailsSent" /> ends, or <c>null</c>
 168    ///     before the first send. The first send after it ends opens a new window.
 169    /// </summary>
 382170    public DateTime? AccountEmailWindowEndsAt { get; set; }
 171
 172    /// <summary>
 173    ///     When the last of those sends was reserved, which the per-person cooldown is measured from
 174    ///     (FR-PE-16), or <c>null</c> before the first.
 175    /// </summary>
 382176    public DateTime? AccountEmailLastSentAt { get; set; }
 177
 178    /// <summary>
 179    ///     A random value every token issued to this person carries, and must still carry when it is
 180    ///     presented (FR-AU-11). Replaced whenever a credential changes — the password, the email, or
 181    ///     the second factor — which revokes every token issued before the change.
 182    /// </summary>
 183    /// <remarks>
 184    ///     Replaced only through <c>IAtomicWrites.RotateSecurityStampAsync</c>, and never written by a
 185    ///     whole-row save: the mapping ignores it after insert. A save of a copy read before a
 186    ///     rotation would otherwise write the old stamp back, and with it every token the rotation
 187    ///     revoked.
 188    /// </remarks>
 3230189    public Guid SecurityStamp { get; set; } = Guid.NewGuid();
 190
 191    /// <summary>Foreign key to the associated <c>Role</c> (internal Id). Required.</summary>
 3154192    public long RoleId { get; set; }
 193
 194    /// <summary>
 195    ///     The scope a <c>User</c> belongs to (internal Id), or <c>null</c> for a <c>ScopeAdmin</c>
 196    ///     or <c>SystemAdmin</c>. A copy of <see cref="ScopeMembership" />'s <c>ScopeId</c>, not a
 197    ///     second source of truth: <c>SCOPE_USER</c> remains the relationship (§4.6). A read that only
 198    ///     needs to know which scope a person is in may take this column directly — the email
 199    ///     verification and password-reset services do, to find the scope's link bases — because it
 200    ///     mirrors the membership at every place that writes either.
 201    /// </summary>
 202    /// <remarks>
 203    ///     This exists for one reason — FR-PE-09's per-scope rule cannot otherwise be enforced. The
 204    ///     scope lives in <c>SCOPE_USER</c> and the address in <c>PERSON</c>, and a PostgreSQL unique
 205    ///     index covers one table, so the rule was left to a check-then-insert that two concurrent
 206    ///     creates both pass. Carrying the scope here lets the index be written over columns this
 207    ///     table already has — <c>role_id</c>, <c>is_deleted</c>, <c>email</c> — with a condition
 208    ///     matching the application's check exactly, so nothing about the rule changes; only who
 209    ///     enforces it does.
 210    ///
 211    ///     Kept in step with <see cref="ScopeMembership" /> at the three places that write it:
 212    ///     UC-06 path a sets both, UC-23 and UC-08's role change clear both.
 213    /// </remarks>
 669214    public long? ScopeId { get; set; }
 215
 216
 217    /// <summary>
 218    ///     The lawful basis this identity's data is processed on (see <c>LegalBases</c>), recorded
 219    ///     when the identity was created (GDPR Art. 5(2), LGPD Art. 8 §2).
 220    /// </summary>
 269221    public int LegalBasis { get; set; }
 222
 223    /// <summary>
 224    ///     The version of the privacy notice in force when the identity was created, or <c>null</c>
 225    ///     for identities predating the mechanism.
 226    /// </summary>
 227    /// <remarks>
 228    ///     Stored rather than looked up, for the same reason the erasure deadline is: what the
 229    ///     person was told is a fact about a moment, and reading the current version later would
 230    ///     answer a different question — what they would be told today.
 231    /// </remarks>
 232    [MaxLength(50)]
 263233    public string? PrivacyNoticeVersion { get; set; }
 234
 235    /// <summary>When the basis and notice version were recorded.</summary>
 264236    public DateTime? BasisRecordedAt { get; set; }
 237
 238    /// <summary>
 239    ///     When processing of this identity was restricted (GDPR Art. 18, LGPD Art. 18 III–IV), or
 240    ///     <c>null</c> if it is not restricted.
 241    /// </summary>
 242    /// <remarks>
 243    ///     Independent of <see cref="IsDeleted" />, and necessarily so. Deletion means the identity
 244    ///     is on its way out; restriction means it is disputed and must be preserved exactly as it
 245    ///     is while that is resolved. Overloading the deletion flag would make a restricted record
 246    ///     disappear from the administrator's view that has to resolve the dispute.
 247    /// </remarks>
 1003248    public DateTime? ProcessingRestrictedAt { get; set; }
 249
 250    /// <summary>
 251    ///     Which of Art. 18(1)'s four grounds the restriction rests on (see
 252    ///     <c>RestrictionGrounds</c>), or <c>null</c> if not restricted.
 253    /// </summary>
 319254    public int? RestrictionGround { get; set; }
 255
 256    /// <summary>
 257    ///     When the subject was told the restriction was about to be lifted, or <c>null</c>.
 258    /// </summary>
 259    /// <remarks>
 260    ///     GDPR Art. 18(3) requires the subject be informed <em>before</em> a restriction imposed at
 261    ///     their request is lifted. This records that it happened, and the lift refuses to proceed
 262    ///     without it.
 263    /// </remarks>
 214264    public DateTime? RestrictionLiftNotifiedAt { get; set; }
 265
 266
 267    /// <summary>Creation timestamp.</summary>
 397268    public DateTime CreatedAt { get; set; }
 269
 270    /// <summary>Last update timestamp.</summary>
 574271    public DateTime UpdatedAt { get; set; }
 272
 273    // Navigation properties
 274
 275    /// <summary>The role that classifies this person.</summary>
 192276    public Role Role { get; set; } = null!;
 277
 278    /// <summary>
 279    ///     Scope-ownership join rows. Non-empty for a <c>ScopeAdmin</c> (one per owned scope);
 280    ///     empty for a <c>User</c> or <c>SystemAdmin</c>.
 281    /// </summary>
 3516282    public ICollection<ScopeOwner> ScopeOwnerships { get; set; } = new List<ScopeOwner>();
 283
 284    /// <summary>
 285    ///     Scope-membership join row. Present for a <c>User</c> (exactly one); <c>null</c> for a
 286    ///     <c>ScopeAdmin</c> or <c>SystemAdmin</c>.
 287    /// </summary>
 955288    public ScopeUser? ScopeMembership { get; set; }
 289
 290    /// <summary>Applications owned by this person.</summary>
 2654291    public ICollection<Application> OwnedApplications { get; set; } = new List<Application>();
 292
 293    /// <summary>Password reset tokens issued for this person.</summary>
 2654294    public ICollection<PasswordResetToken> PasswordResetTokens { get; set; } = new List<PasswordResetToken>();
 295
 296    /// <summary>Email verification tokens issued for this person.</summary>
 2654297    public ICollection<EmailVerificationToken> EmailVerificationTokens { get; set; } = new List<EmailVerificationToken>(
 298
 299    /// <summary>
 300    ///     This person's two-factor authentication configuration (UC-36 – UC-40). <c>null</c> until
 301    ///     UC-36 initiates setup.
 302    /// </summary>
 275303    public TwoFactorAuth? TwoFactorAuth { get; set; }
 304}