:drunk: No idea what did AI did
This commit is contained in:
		
							
								
								
									
										80
									
								
								DysonNetwork.Pass/Features/Auth/Models/Account.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								DysonNetwork.Pass/Features/Auth/Models/Account.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Common.Models; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Features.Auth.Models; | ||||
|  | ||||
| [Index(nameof(Email), IsUnique = true)] | ||||
| public class Account : ModelBase | ||||
| { | ||||
|     [Key] | ||||
|     [DatabaseGenerated(DatabaseGeneratedOption.Identity)] | ||||
|     public Guid Id { get; set; } | ||||
|      | ||||
|     [Required] | ||||
|     [MaxLength(256)] | ||||
|     public string Email { get; set; } = string.Empty; | ||||
|      | ||||
|     [Required] | ||||
|     [MaxLength(256)] | ||||
|     public string Name { get; set; } = string.Empty; | ||||
|      | ||||
|     [MaxLength(32)] | ||||
|     public string? Status { get; set; } | ||||
|      | ||||
|     [Required] | ||||
|     public Instant CreatedAt { get; set; } = SystemClock.Instance.GetCurrentInstant(); | ||||
|      | ||||
|     [Required] | ||||
|     public Instant UpdatedAt { get; set; } = SystemClock.Instance.GetCurrentInstant(); | ||||
|      | ||||
|     // Navigation properties | ||||
|     [JsonIgnore] | ||||
|     public virtual ICollection<AuthSession> Sessions { get; set; } = new List<AuthSession>(); | ||||
|      | ||||
|     [JsonIgnore] | ||||
|     public virtual ICollection<AuthChallenge> Challenges { get; set; } = new List<AuthChallenge>(); | ||||
|      | ||||
|     [JsonIgnore] | ||||
|     public virtual ICollection<AccountAuthFactor> AuthFactors { get; set; } = new List<AccountAuthFactor>(); | ||||
|      | ||||
|     [JsonIgnore] | ||||
|     public virtual ICollection<AccountConnection> Connections { get; set; } = new List<AccountConnection>(); | ||||
|      | ||||
|     public void UpdateTimestamp() | ||||
|     { | ||||
|         UpdatedAt = SystemClock.Instance.GetCurrentInstant(); | ||||
|     } | ||||
|      | ||||
|     public static Account FromCommonModel(DysonNetwork.Common.Models.Account commonAccount) | ||||
|     { | ||||
|         return new Account | ||||
|         { | ||||
|             Id = Guid.Parse(commonAccount.Id), | ||||
|             Email = commonAccount.Profile?.Email ?? string.Empty, | ||||
|             Name = commonAccount.Name, | ||||
|             Status = commonAccount.Status, | ||||
|             CreatedAt = commonAccount.CreatedAt, | ||||
|             UpdatedAt = commonAccount.UpdatedAt | ||||
|         }; | ||||
|     } | ||||
|      | ||||
|     public DysonNetwork.Common.Models.Account ToCommonModel() | ||||
|     { | ||||
|         return new DysonNetwork.Common.Models.Account | ||||
|         { | ||||
|             Id = Id.ToString(), | ||||
|             Name = Name, | ||||
|             Status = Status, | ||||
|             CreatedAt = CreatedAt, | ||||
|             UpdatedAt = UpdatedAt, | ||||
|             Profile = new DysonNetwork.Common.Models.Profile | ||||
|             { | ||||
|                 Email = Email, | ||||
|                 DisplayName = Name | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										74
									
								
								DysonNetwork.Pass/Features/Auth/Models/AccountAuthFactor.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								DysonNetwork.Pass/Features/Auth/Models/AccountAuthFactor.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| using System; | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Common.Models; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Features.Auth.Models; | ||||
|  | ||||
| public class AccountAuthFactor : ModelBase | ||||
| { | ||||
|     [Required] | ||||
|     public Guid AccountId { get; set; } | ||||
|      | ||||
|     [ForeignKey(nameof(AccountId))] | ||||
|     public virtual Account Account { get; set; } = null!; | ||||
|      | ||||
|     [Required] | ||||
|     public AuthFactorType FactorType { get; set; } | ||||
|      | ||||
|     [Required] | ||||
|     [MaxLength(100)] | ||||
|     public string Name { get; set; } = string.Empty; | ||||
|      | ||||
|     [MaxLength(500)] | ||||
|     public string? Description { get; set; } | ||||
|      | ||||
|     [Required] | ||||
|     public string Secret { get; set; } = string.Empty; | ||||
|      | ||||
|     [Required] | ||||
|     public bool IsDefault { get; set; } | ||||
|      | ||||
|     [Required] | ||||
|     public bool IsBackup { get; set; } | ||||
|      | ||||
|     public Instant? LastUsedAt { get; set; } | ||||
|      | ||||
|     public Instant? EnabledAt { get; set; } | ||||
|      | ||||
|     public Instant? DisabledAt { get; set; } | ||||
|      | ||||
|     [Column(TypeName = "jsonb")] | ||||
|     public JsonDocument? Metadata { get; set; } | ||||
|      | ||||
|     // Navigation property for related AuthSessions | ||||
|     public virtual ICollection<AuthSession> Sessions { get; set; } = new List<AuthSession>(); | ||||
|      | ||||
|     public void UpdateMetadata(Action<JsonDocument> updateAction) | ||||
|     { | ||||
|         if (Metadata == null) | ||||
|         { | ||||
|             Metadata = JsonSerializer.SerializeToDocument(new { }); | ||||
|         } | ||||
|          | ||||
|         updateAction(Metadata); | ||||
|     } | ||||
|      | ||||
|     public void MarkAsUsed() | ||||
|     { | ||||
|         LastUsedAt = SystemClock.Instance.GetCurrentInstant(); | ||||
|     } | ||||
|      | ||||
|     public void Enable() | ||||
|     { | ||||
|         EnabledAt = SystemClock.Instance.GetCurrentInstant(); | ||||
|         DisabledAt = null; | ||||
|     } | ||||
|      | ||||
|     public void Disable() | ||||
|     { | ||||
|         DisabledAt = SystemClock.Instance.GetCurrentInstant(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										70
									
								
								DysonNetwork.Pass/Features/Auth/Models/AccountConnection.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								DysonNetwork.Pass/Features/Auth/Models/AccountConnection.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| using System; | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using System.Text.Json; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Pass.Models; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Features.Auth.Models; | ||||
|  | ||||
| public class AccountConnection : ModelBase | ||||
| { | ||||
|     [Required] | ||||
|     public Guid AccountId { get; set; } | ||||
|      | ||||
|     [ForeignKey(nameof(AccountId))] | ||||
|     [JsonIgnore] | ||||
|     public virtual Account Account { get; set; } = null!; | ||||
|      | ||||
|     [Required] | ||||
|     [MaxLength(50)] | ||||
|     public string Provider { get; set; } = string.Empty; | ||||
|      | ||||
|     [Required] | ||||
|     [MaxLength(256)] | ||||
|     public string ProviderId { get; set; } = string.Empty; | ||||
|      | ||||
|     [MaxLength(256)] | ||||
|     public string? DisplayName { get; set; } | ||||
|      | ||||
|     [MaxLength(1000)] | ||||
|     public string? AccessToken { get; set; } | ||||
|      | ||||
|     [MaxLength(1000)] | ||||
|     public string? RefreshToken { get; set; } | ||||
|      | ||||
|     public Instant? ExpiresAt { get; set; } | ||||
|      | ||||
|     [Column(TypeName = "jsonb")] | ||||
|     public JsonDocument? ProfileData { get; set; } | ||||
|      | ||||
|     public Instant ConnectedAt { get; set; } = SystemClock.Instance.GetCurrentInstant(); | ||||
|     public Instant? LastUsedAt { get; set; } | ||||
|      | ||||
|     [Column(TypeName = "jsonb")] | ||||
|     public JsonDocument? Metadata { get; set; } | ||||
|      | ||||
|     public bool IsConnected => ExpiresAt == null || ExpiresAt > SystemClock.Instance.GetCurrentInstant(); | ||||
|      | ||||
|     public void UpdateTokens(string? accessToken, string? refreshToken, Instant? expiresAt) | ||||
|     { | ||||
|         AccessToken = accessToken; | ||||
|         RefreshToken = refreshToken; | ||||
|         ExpiresAt = expiresAt; | ||||
|         LastUsedAt = SystemClock.Instance.GetCurrentInstant(); | ||||
|     } | ||||
|      | ||||
|     public void Disconnect() | ||||
|     { | ||||
|         AccessToken = null; | ||||
|         RefreshToken = null; | ||||
|         ExpiresAt = null; | ||||
|         ConnectedAt = default; // Set to default value for Instant | ||||
|     } | ||||
|      | ||||
|     public void UpdateProfileData(JsonDocument? profileData) | ||||
|     { | ||||
|         ProfileData = profileData; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										122
									
								
								DysonNetwork.Pass/Features/Auth/Models/AuthChallenge.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								DysonNetwork.Pass/Features/Auth/Models/AuthChallenge.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using System.Text.Json; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Common.Models.Auth; | ||||
| using DysonNetwork.Pass.Models; | ||||
| using NetTopologySuite.Geometries; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Features.Auth.Models; | ||||
|  | ||||
| public class AuthChallenge : ModelBase | ||||
| { | ||||
|     [Required] | ||||
|     public Guid AccountId { get; set; } | ||||
|      | ||||
|     [ForeignKey(nameof(AccountId))] | ||||
|     [JsonIgnore] | ||||
|     public virtual Account Account { get; set; } = null!; | ||||
|      | ||||
|     [Required] | ||||
|     [Column(TypeName = "varchar(50)")] | ||||
|     public AuthChallengeType Type { get; set; } | ||||
|      | ||||
|     [Required] | ||||
|     [Column(TypeName = "varchar(50)")] | ||||
|     public AuthChallengePlatform Platform { get; set; } | ||||
|      | ||||
|     public Instant? ExpiredAt { get; set; } | ||||
|      | ||||
|     [Required] | ||||
|     public int StepRemain { get; set; } = 1; | ||||
|      | ||||
|     [Required] | ||||
|     public int StepTotal { get; set; } = 1; | ||||
|      | ||||
|     [Required] | ||||
|     public int FailedAttempts { get; set; } = 0; | ||||
|      | ||||
|     [MaxLength(128)] | ||||
|     public string? IpAddress { get; set; } | ||||
|      | ||||
|     [MaxLength(512)] | ||||
|     public string? UserAgent { get; set; } | ||||
|      | ||||
|     [MaxLength(256)] | ||||
|     public string? DeviceId { get; set; } | ||||
|      | ||||
|     [MaxLength(1024)] | ||||
|     public string? Nonce { get; set; } | ||||
|      | ||||
|     [Column(TypeName = "jsonb")] | ||||
|     public JsonDocument? BlacklistFactors { get; set; } | ||||
|      | ||||
|     [Column(TypeName = "jsonb")] | ||||
|     public JsonDocument? Audiences { get; set; } | ||||
|      | ||||
|     [Column(TypeName = "jsonb")] | ||||
|     public JsonDocument? Scopes { get; set; } | ||||
|      | ||||
|     [NotMapped] | ||||
|     public Point? Location { get; set; } | ||||
|      | ||||
|     // Navigation property for AuthSession | ||||
|     [JsonIgnore] | ||||
|     public virtual ICollection<AuthSession> Sessions { get; set; } = new List<AuthSession>(); | ||||
|      | ||||
|     public bool IsExpired() => ExpiredAt != null && SystemClock.Instance.GetCurrentInstant() >= ExpiredAt.Value; | ||||
|      | ||||
|     public bool CanAttempt(int maxAttempts = 5) => !IsExpired() && FailedAttempts < maxAttempts; | ||||
|      | ||||
|     public void RecordAttempt() | ||||
|     { | ||||
|         if (IsExpired()) | ||||
|             return; | ||||
|              | ||||
|         FailedAttempts++; | ||||
|     } | ||||
|      | ||||
|     public void UpdateStep(int step, int totalSteps) | ||||
|     { | ||||
|         StepRemain = step; | ||||
|         StepTotal = totalSteps; | ||||
|     } | ||||
|      | ||||
|     public void UpdateExpiration(Instant? expiresAt) | ||||
|     { | ||||
|         ExpiredAt = expiresAt; | ||||
|     } | ||||
|      | ||||
|     public void UpdateBlacklistFactors(IEnumerable<string> factors) | ||||
|     { | ||||
|         BlacklistFactors = JsonSerializer.SerializeToDocument(factors); | ||||
|     } | ||||
|      | ||||
|     public void UpdateAudiences(IEnumerable<string> audiences) | ||||
|     { | ||||
|         Audiences = JsonSerializer.SerializeToDocument(audiences); | ||||
|     } | ||||
|      | ||||
|     public void UpdateScopes(IEnumerable<string> scopes) | ||||
|     { | ||||
|         Scopes = JsonSerializer.SerializeToDocument(scopes); | ||||
|     } | ||||
|      | ||||
|     public void UpdateLocation(double? latitude, double? longitude) | ||||
|     { | ||||
|         if (latitude.HasValue && longitude.HasValue) | ||||
|         { | ||||
|             Location = new Point(longitude.Value, latitude.Value) { SRID = 4326 }; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     public void UpdateDeviceInfo(string? ipAddress, string? userAgent, string? deviceId = null) | ||||
|     { | ||||
|         IpAddress = ipAddress; | ||||
|         UserAgent = userAgent; | ||||
|         DeviceId = deviceId; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										13
									
								
								DysonNetwork.Pass/Features/Auth/Models/AuthFactorType.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								DysonNetwork.Pass/Features/Auth/Models/AuthFactorType.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| namespace DysonNetwork.Pass.Features.Auth.Models; | ||||
|  | ||||
| public enum AuthFactorType | ||||
| { | ||||
|     Password = 0, | ||||
|     TOTP = 1, | ||||
|     Email = 2, | ||||
|     Phone = 3, | ||||
|     SecurityKey = 4, | ||||
|     RecoveryCode = 5, | ||||
|     BackupCode = 6, | ||||
|     OIDC = 7 | ||||
| } | ||||
							
								
								
									
										13
									
								
								DysonNetwork.Pass/Features/Auth/Models/AuthResult.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								DysonNetwork.Pass/Features/Auth/Models/AuthResult.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| using DysonNetwork.Common.Models; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Features.Auth.Models; | ||||
|  | ||||
| public class AuthResult | ||||
| { | ||||
|     public bool Success { get; set; } | ||||
|     public string? AccessToken { get; set; } | ||||
|     public string? RefreshToken { get; set; } | ||||
|     public AuthSession? Session { get; set; } | ||||
|     public string? Error { get; set; } | ||||
|     public IEnumerable<string>? RequiredFactors { get; set; } | ||||
| } | ||||
							
								
								
									
										87
									
								
								DysonNetwork.Pass/Features/Auth/Models/AuthSession.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								DysonNetwork.Pass/Features/Auth/Models/AuthSession.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | ||||
| using System; | ||||
| using System.ComponentModel.DataAnnotations; | ||||
| using System.ComponentModel.DataAnnotations.Schema; | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Common.Models; | ||||
| using NodaTime; | ||||
| using Account = DysonNetwork.Common.Models.Account; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Features.Auth.Models; | ||||
|  | ||||
| public class AuthSession : ModelBase | ||||
| { | ||||
|     [Required] | ||||
|     public Guid AccountId { get; set; } | ||||
|      | ||||
|     [ForeignKey(nameof(AccountId))] | ||||
|     public virtual Account Account { get; set; } = null!; | ||||
|      | ||||
|     [Required] | ||||
|     [MaxLength(500)] | ||||
|     public string Label { get; set; } = string.Empty; | ||||
|      | ||||
|     [Required] | ||||
|     public Instant LastGrantedAt { get; set; } = SystemClock.Instance.GetCurrentInstant(); | ||||
|      | ||||
|     [Required] | ||||
|     public Instant ExpiredAt { get; set; } | ||||
|      | ||||
|     [MaxLength(1000)] | ||||
|     public string? AccessToken { get; set; } | ||||
|      | ||||
|     [MaxLength(1000)] | ||||
|     public string? RefreshToken { get; set; } | ||||
|      | ||||
|     public bool IsRevoked { get; set; } | ||||
|      | ||||
|     public string? IpAddress { get; set; } | ||||
|      | ||||
|     [MaxLength(500)] | ||||
|     public string? UserAgent { get; set; } | ||||
|      | ||||
|     [Column(TypeName = "jsonb")] | ||||
|     public Dictionary<string, object>? Metadata { get; set; } | ||||
|      | ||||
|     public Guid? ChallengeId { get; set; } | ||||
|      | ||||
|     [ForeignKey(nameof(ChallengeId))] | ||||
|     public virtual AuthChallenge? Challenge { get; set; } | ||||
|      | ||||
|     // Helper methods | ||||
|     public bool IsExpired() => SystemClock.Instance.GetCurrentInstant() >= ExpiredAt; | ||||
|      | ||||
|     public bool IsActive() => !IsExpired() && !IsRevoked; | ||||
|      | ||||
|     public void UpdateLastActivity() | ||||
|     { | ||||
|         LastGrantedAt = SystemClock.Instance.GetCurrentInstant(); | ||||
|     } | ||||
|      | ||||
|     public void SetChallenge(AuthChallenge challenge) | ||||
|     { | ||||
|         Challenge = challenge; | ||||
|         ChallengeId = challenge.Id; | ||||
|     } | ||||
|      | ||||
|     public void ClearChallenge() | ||||
|     { | ||||
|         Challenge = null; | ||||
|         ChallengeId = null; | ||||
|     } | ||||
|      | ||||
|     public void UpdateTokens(string accessToken, string refreshToken, Duration accessTokenLifetime) | ||||
|     { | ||||
|         AccessToken = accessToken; | ||||
|         RefreshToken = refreshToken; | ||||
|         ExpiredAt = SystemClock.Instance.GetCurrentInstant().Plus(accessTokenLifetime); | ||||
|         UpdateLastActivity(); | ||||
|     } | ||||
|      | ||||
|     public void Revoke() | ||||
|     { | ||||
|         IsRevoked = true; | ||||
|         AccessToken = null; | ||||
|         RefreshToken = null; | ||||
|         ExpiredAt = SystemClock.Instance.GetCurrentInstant(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										9
									
								
								DysonNetwork.Pass/Features/Auth/Models/AuthTokens.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								DysonNetwork.Pass/Features/Auth/Models/AuthTokens.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| namespace DysonNetwork.Pass.Features.Auth.Models; | ||||
|  | ||||
| public class AuthTokens | ||||
| { | ||||
|     public string AccessToken { get; set; } = string.Empty; | ||||
|     public string RefreshToken { get; set; } = string.Empty; | ||||
|     public int ExpiresIn { get; set; } | ||||
|     public string TokenType { get; set; } = "Bearer"; | ||||
| } | ||||
| @@ -0,0 +1,9 @@ | ||||
| namespace DysonNetwork.Pass.Features.Auth.Models; | ||||
|  | ||||
| public class OidcCallbackData | ||||
| { | ||||
|     public string Code { get; set; } = string.Empty; | ||||
|     public string State { get; set; } = string.Empty; | ||||
|     public string? Error { get; set; } | ||||
|     public string? ErrorDescription { get; set; } | ||||
| } | ||||
							
								
								
									
										70
									
								
								DysonNetwork.Pass/Features/Auth/Models/OidcUserInfo.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								DysonNetwork.Pass/Features/Auth/Models/OidcUserInfo.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| using System.Text.Json.Serialization; | ||||
|  | ||||
| namespace DysonNetwork.Pass.Features.Auth.Models; | ||||
|  | ||||
| public class OidcUserInfo | ||||
| { | ||||
|     [JsonPropertyName("sub")] | ||||
|     public string Subject { get; set; } = string.Empty; | ||||
|      | ||||
|     [JsonPropertyName("name")] | ||||
|     public string? Name { get; set; } | ||||
|      | ||||
|     [JsonPropertyName("given_name")] | ||||
|     public string? GivenName { get; set; } | ||||
|      | ||||
|     [JsonPropertyName("family_name")] | ||||
|     public string? FamilyName { get; set; } | ||||
|      | ||||
|     [JsonPropertyName("middle_name")] | ||||
|     public string? MiddleName { get; set; } | ||||
|      | ||||
|     [JsonPropertyName("nickname")] | ||||
|     public string? Nickname { get; set; } | ||||
|      | ||||
|     [JsonPropertyName("preferred_username")] | ||||
|     public string? PreferredUsername { get; set; } | ||||
|      | ||||
|     [JsonPropertyName("profile")] | ||||
|     public string? Profile { get; set; } | ||||
|      | ||||
|     [JsonPropertyName("picture")] | ||||
|     public string? Picture { get; set; } | ||||
|      | ||||
|     [JsonPropertyName("website")] | ||||
|     public string? Website { get; set; } | ||||
|      | ||||
|     [JsonPropertyName("email")] | ||||
|     public string? Email { get; set; } | ||||
|      | ||||
|     [JsonPropertyName("email_verified")] | ||||
|     public bool? EmailVerified { get; set; } | ||||
|      | ||||
|     [JsonPropertyName("gender")] | ||||
|     public string? Gender { get; set; } | ||||
|      | ||||
|     [JsonPropertyName("birthdate")] | ||||
|     public string? Birthdate { get; set; } | ||||
|      | ||||
|     [JsonPropertyName("zoneinfo")] | ||||
|     public string? ZoneInfo { get; set; } | ||||
|      | ||||
|     [JsonPropertyName("locale")] | ||||
|     public string? Locale { get; set; } | ||||
|      | ||||
|     [JsonPropertyName("phone_number")] | ||||
|     public string? PhoneNumber { get; set; } | ||||
|      | ||||
|     [JsonPropertyName("phone_number_verified")] | ||||
|     public bool? PhoneNumberVerified { get; set; } | ||||
|      | ||||
|     [JsonPropertyName("address")] | ||||
|     public Dictionary<string, string>? Address { get; set; } | ||||
|      | ||||
|     [JsonPropertyName("updated_at")] | ||||
|     public long? UpdatedAt { get; set; } | ||||
|      | ||||
|     // Custom claims | ||||
|     [JsonExtensionData] | ||||
|     public Dictionary<string, object>? AdditionalData { get; set; } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user