| | | 1 | | using System.ComponentModel.DataAnnotations; |
| | | 2 | | using ArturRios.Data.Relational.Core.Entities; |
| | | 3 | | |
| | | 4 | | namespace ArturRios.Heimdall.Domain.Entities; |
| | | 5 | | |
| | | 6 | | /// <summary> |
| | | 7 | | /// A registered identity authenticated via Google Sign-In rather than a password. Always |
| | | 8 | | /// equivalent to the <c>User</c> role and belongs to exactly one <see cref="Scope" />. Stored |
| | | 9 | | /// in its own table with a direct <see cref="ScopeId" /> instead of going through the |
| | | 10 | | /// owner/user join tables, since it never needs ownership or multi-scope semantics. Has no |
| | | 11 | | /// <c>PasswordHash</c>, <c>Salt</c>, or <c>RoleId</c> — authentication is delegated to Google. |
| | | 12 | | /// </summary> |
| | | 13 | | public class GoogleUser : Entity<long> |
| | | 14 | | { |
| | | 15 | | /// <summary> |
| | | 16 | | /// External identifier, generated on creation and used everywhere the Google User is |
| | | 17 | | /// addressed from outside the database (API paths, response bodies, token claims). The |
| | | 18 | | /// internal <see cref="Entity{TKey}.Id" /> is never exposed to callers. |
| | | 19 | | /// </summary> |
| | 732 | 20 | | public Guid PublicId { get; set; } = Guid.NewGuid(); |
| | | 21 | | |
| | | 22 | | /// <summary>Google's stable <c>sub</c> claim. Required, unique within the scope.</summary> |
| | | 23 | | [MaxLength(255)] |
| | 497 | 24 | | public string GoogleId { get; set; } = string.Empty; |
| | | 25 | | |
| | | 26 | | /// <summary>Full name, from Google's <c>name</c> claim. Max 200 characters.</summary> |
| | | 27 | | [MaxLength(200)] |
| | 482 | 28 | | public string Name { get; set; } = string.Empty; |
| | | 29 | | |
| | | 30 | | /// <summary> |
| | | 31 | | /// Email address, from Google's <c>email</c> claim. Required, unique within the scope, |
| | | 32 | | /// considered jointly with <c>User</c> persons' emails in that scope. |
| | | 33 | | /// </summary> |
| | | 34 | | [MaxLength(256)] |
| | 496 | 35 | | public string Email { get; set; } = string.Empty; |
| | | 36 | | |
| | | 37 | | /// <summary>Email verification status, from Google's <c>email_verified</c> claim.</summary> |
| | 240 | 38 | | public bool EmailVerified { get; set; } |
| | | 39 | | |
| | | 40 | | /// <summary>Optional profile picture URL, from Google's <c>picture</c> claim.</summary> |
| | | 41 | | [MaxLength(2048)] |
| | 106 | 42 | | public string? ProfilePictureUrl { get; set; } |
| | | 43 | | |
| | | 44 | | /// <summary>Logical deletion flag. Logically deleted Google Users are excluded from default queries.</summary> |
| | 276 | 45 | | public bool IsDeleted { get; set; } |
| | | 46 | | |
| | | 47 | | /// <summary> |
| | | 48 | | /// When the record was logically deleted, or <c>null</c> while it is live. The retention |
| | | 49 | | /// window that ends in anonymisation is measured from here (NFR-20). |
| | | 50 | | /// </summary> |
| | | 51 | | /// <remarks> |
| | | 52 | | /// <see cref="UpdatedAt" /> cannot serve: it moves on any later write, so a record touched |
| | | 53 | | /// after its deletion would have its window silently restarted. This one is written once, by |
| | | 54 | | /// the logical deletion, and never again. |
| | | 55 | | /// </remarks> |
| | 47 | 56 | | public DateTime? DeletedAt { get; set; } |
| | | 57 | | |
| | | 58 | | /// <summary> |
| | | 59 | | /// Why the record was deleted (see <c>DeletionKinds</c>), or <c>null</c> while it is live. |
| | | 60 | | /// Decides which of the schedule's two deadlines applies. |
| | | 61 | | /// </summary> |
| | 45 | 62 | | public int? DeletionKind { get; set; } |
| | | 63 | | |
| | | 64 | | /// <summary> |
| | | 65 | | /// When the record was anonymised, or <c>null</c> if it has not been. Set by the scheduled |
| | | 66 | | /// pass, and the reason a second pass over the same row is a no-op rather than a repeated |
| | | 67 | | /// overwrite. |
| | | 68 | | /// </summary> |
| | 66 | 69 | | public DateTime? AnonymisedAt { get; set; } |
| | | 70 | | |
| | | 71 | | /// <summary> |
| | | 72 | | /// When the data subject asked to be erased (UC-42), or <c>null</c> if they have not. Set |
| | | 73 | | /// once, by the request, and never moved: the statutory deadline runs from the request, not |
| | | 74 | | /// from any later event. |
| | | 75 | | /// </summary> |
| | 42 | 76 | | public DateTime? ErasureRequestedAt { get; set; } |
| | | 77 | | |
| | | 78 | | /// <summary> |
| | | 79 | | /// The deadline the erasure must not outlive, computed when the request is made. |
| | | 80 | | /// </summary> |
| | | 81 | | /// <remarks> |
| | | 82 | | /// Stored rather than derived on read, and that is the point. GDPR Art. 12(3) attaches an |
| | | 83 | | /// obligation at the moment of the request; recomputing it from the current configuration |
| | | 84 | | /// would let a later change to the deadline move an obligation that had already attached — |
| | | 85 | | /// forward, which is unlawful, or backward, which would silently mark existing requests |
| | | 86 | | /// overdue. |
| | | 87 | | /// </remarks> |
| | 29 | 88 | | public DateTime? ErasureDueAt { get; set; } |
| | | 89 | | |
| | | 90 | | /// <summary> |
| | | 91 | | /// Why the erasure could not be carried out yet, or <c>null</c> if nothing blocks it. One of |
| | | 92 | | /// the application's own canonical messages, never caller input. |
| | | 93 | | /// </summary> |
| | | 94 | | /// <remarks> |
| | | 95 | | /// A blocked request is still a request: the deadline keeps running, and this is what makes |
| | | 96 | | /// the reason visible to the administrator who has to clear it, rather than the request |
| | | 97 | | /// failing and leaving no trace. |
| | | 98 | | /// </remarks> |
| | | 99 | | [MaxLength(500)] |
| | 24 | 100 | | public string? ErasureBlockedReason { get; set; } |
| | | 101 | | |
| | | 102 | | /// <summary>Foreign key to the associated <see cref="Scope" /> (internal Id). Required.</summary> |
| | 273 | 103 | | public long ScopeId { get; set; } |
| | | 104 | | |
| | | 105 | | |
| | | 106 | | /// <summary> |
| | | 107 | | /// The lawful basis this identity's data is processed on (see <c>LegalBases</c>), recorded |
| | | 108 | | /// when the identity was created (GDPR Art. 5(2), LGPD Art. 8 §2). |
| | | 109 | | /// </summary> |
| | 45 | 110 | | public int LegalBasis { get; set; } |
| | | 111 | | |
| | | 112 | | /// <summary> |
| | | 113 | | /// The version of the privacy notice in force when the identity was created, or <c>null</c> |
| | | 114 | | /// for identities predating the mechanism. |
| | | 115 | | /// </summary> |
| | | 116 | | /// <remarks> |
| | | 117 | | /// Stored rather than looked up, for the same reason the erasure deadline is: what the |
| | | 118 | | /// person was told is a fact about a moment, and reading the current version later would |
| | | 119 | | /// answer a different question — what they would be told today. |
| | | 120 | | /// </remarks> |
| | | 121 | | [MaxLength(50)] |
| | 43 | 122 | | public string? PrivacyNoticeVersion { get; set; } |
| | | 123 | | |
| | | 124 | | /// <summary>When the basis and notice version were recorded.</summary> |
| | 44 | 125 | | public DateTime? BasisRecordedAt { get; set; } |
| | | 126 | | |
| | | 127 | | /// <summary> |
| | | 128 | | /// When processing of this identity was restricted (GDPR Art. 18, LGPD Art. 18 III–IV), or |
| | | 129 | | /// <c>null</c> if it is not restricted. |
| | | 130 | | /// </summary> |
| | | 131 | | /// <remarks> |
| | | 132 | | /// Independent of <see cref="IsDeleted" />, and necessarily so. Deletion means the identity |
| | | 133 | | /// is on its way out; restriction means it is disputed and must be preserved exactly as it |
| | | 134 | | /// is while that is resolved. Overloading the deletion flag would make a restricted record |
| | | 135 | | /// disappear from the administrator's view that has to resolve the dispute. |
| | | 136 | | /// </remarks> |
| | 83 | 137 | | public DateTime? ProcessingRestrictedAt { get; set; } |
| | | 138 | | |
| | | 139 | | /// <summary> |
| | | 140 | | /// Which of Art. 18(1)'s four grounds the restriction rests on (see |
| | | 141 | | /// <c>RestrictionGrounds</c>), or <c>null</c> if not restricted. |
| | | 142 | | /// </summary> |
| | 29 | 143 | | public int? RestrictionGround { get; set; } |
| | | 144 | | |
| | | 145 | | /// <summary> |
| | | 146 | | /// When the subject was told the restriction was about to be lifted, or <c>null</c>. |
| | | 147 | | /// </summary> |
| | | 148 | | /// <remarks> |
| | | 149 | | /// GDPR Art. 18(3) requires the subject be informed <em>before</em> a restriction imposed at |
| | | 150 | | /// their request is lifted. This records that it happened, and the lift refuses to proceed |
| | | 151 | | /// without it. |
| | | 152 | | /// </remarks> |
| | 24 | 153 | | public DateTime? RestrictionLiftNotifiedAt { get; set; } |
| | | 154 | | |
| | | 155 | | |
| | | 156 | | /// <summary>Creation timestamp.</summary> |
| | 41 | 157 | | public DateTime CreatedAt { get; set; } |
| | | 158 | | |
| | | 159 | | /// <summary>Last update timestamp.</summary> |
| | 85 | 160 | | public DateTime UpdatedAt { get; set; } |
| | | 161 | | |
| | | 162 | | // Navigation properties |
| | | 163 | | |
| | | 164 | | /// <summary>The scope this Google User belongs to.</summary> |
| | 98 | 165 | | public Scope Scope { get; set; } = null!; |
| | | 166 | | } |