Compare commits
	
		
			3 Commits
		
	
	
		
			f50894a3d1
			...
			c6450757be
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| c6450757be | |||
| 38abe16ba6 | |||
| bf40b51c41 | 
| @@ -119,12 +119,15 @@ public class AccountAuthFactor : ModelBase | |||||||
|     public Guid Id { get; set; } |     public Guid Id { get; set; } | ||||||
|     public AccountAuthFactorType Type { get; set; } |     public AccountAuthFactorType Type { get; set; } | ||||||
|     [JsonIgnore] [MaxLength(8196)] public string? Secret { get; set; } |     [JsonIgnore] [MaxLength(8196)] public string? Secret { get; set; } | ||||||
|     [JsonIgnore] [Column(TypeName = "jsonb")] public Dictionary<string, object>? Config { get; set; } = new(); |  | ||||||
|  |     [JsonIgnore] | ||||||
|  |     [Column(TypeName = "jsonb")] | ||||||
|  |     public Dictionary<string, object>? Config { get; set; } = new(); | ||||||
|  |  | ||||||
|     /// <summary> |     /// <summary> | ||||||
|     /// The trustworthy stands for how safe is this auth factor. |     /// The trustworthy stands for how safe is this auth factor. | ||||||
|     /// Basically, it affects how many steps it can complete in authentication. |     /// Basically, it affects how many steps it can complete in authentication. | ||||||
|     /// Besides, users may need to use some high trustworthy level auth factors when confirming some dangerous operations. |     /// Besides, users may need to use some high-trustworthy level auth factors when confirming some dangerous operations. | ||||||
|     /// </summary> |     /// </summary> | ||||||
|     public int Trustworthy { get; set; } = 1; |     public int Trustworthy { get; set; } = 1; | ||||||
|  |  | ||||||
| @@ -148,6 +151,7 @@ public class AccountAuthFactor : ModelBase | |||||||
|         switch (Type) |         switch (Type) | ||||||
|         { |         { | ||||||
|             case AccountAuthFactorType.Password: |             case AccountAuthFactorType.Password: | ||||||
|  |             case AccountAuthFactorType.PinCode: | ||||||
|                 return BCrypt.Net.BCrypt.Verify(password, Secret); |                 return BCrypt.Net.BCrypt.Verify(password, Secret); | ||||||
|             case AccountAuthFactorType.TimedCode: |             case AccountAuthFactorType.TimedCode: | ||||||
|                 var otp = new Totp(Base32Encoding.ToBytes(Secret)); |                 var otp = new Totp(Base32Encoding.ToBytes(Secret)); | ||||||
| @@ -172,7 +176,8 @@ public enum AccountAuthFactorType | |||||||
|     Password, |     Password, | ||||||
|     EmailCode, |     EmailCode, | ||||||
|     InAppCode, |     InAppCode, | ||||||
|     TimedCode |     TimedCode, | ||||||
|  |     PinCode, | ||||||
| } | } | ||||||
|  |  | ||||||
| public class AccountConnection : ModelBase | public class AccountConnection : ModelBase | ||||||
|   | |||||||
| @@ -257,6 +257,18 @@ public class AccountService( | |||||||
|                     } |                     } | ||||||
|                 }; |                 }; | ||||||
|                 break; |                 break; | ||||||
|  |             case AccountAuthFactorType.PinCode: | ||||||
|  |                 if (string.IsNullOrWhiteSpace(secret)) throw new ArgumentNullException(nameof(secret)); | ||||||
|  |                 if (!secret.All(char.IsDigit) || secret.Length != 6) | ||||||
|  |                     throw new ArgumentException("PIN code must be exactly 6 digits"); | ||||||
|  |                 factor = new AccountAuthFactor | ||||||
|  |                 { | ||||||
|  |                     Type = AccountAuthFactorType.PinCode, | ||||||
|  |                     Trustworthy = 0, // Only for confirming, can't be used for login | ||||||
|  |                     Secret = secret, | ||||||
|  |                     EnabledAt = SystemClock.Instance.GetCurrentInstant(), | ||||||
|  |                 }.HashSecret(); | ||||||
|  |                 break; | ||||||
|             default: |             default: | ||||||
|                 throw new ArgumentOutOfRangeException(nameof(type), type, null); |                 throw new ArgumentOutOfRangeException(nameof(type), type, null); | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -1,11 +1,19 @@ | |||||||
| using System.Security.Cryptography; | using System.Security.Cryptography; | ||||||
| using System.Text.Json; | using System.Text.Json; | ||||||
|  | using DysonNetwork.Sphere.Account; | ||||||
|  | using DysonNetwork.Sphere.Storage; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| using NodaTime; | using NodaTime; | ||||||
|  |  | ||||||
| namespace DysonNetwork.Sphere.Auth; | namespace DysonNetwork.Sphere.Auth; | ||||||
|  |  | ||||||
| public class AuthService(AppDatabase db, IConfiguration config, IHttpClientFactory httpClientFactory, IHttpContextAccessor httpContextAccessor) | public class AuthService( | ||||||
|  |     AppDatabase db, | ||||||
|  |     IConfiguration config, | ||||||
|  |     IHttpClientFactory httpClientFactory, | ||||||
|  |     IHttpContextAccessor httpContextAccessor, | ||||||
|  |     ICacheService cache | ||||||
|  | ) | ||||||
| { | { | ||||||
|     private HttpContext HttpContext => httpContextAccessor.HttpContext!; |     private HttpContext HttpContext => httpContextAccessor.HttpContext!; | ||||||
|  |  | ||||||
| @@ -174,6 +182,69 @@ public class AuthService(AppDatabase db, IConfiguration config, IHttpClientFacto | |||||||
|         return $"{payloadBase64}.{signatureBase64}"; |         return $"{payloadBase64}.{signatureBase64}"; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     public async Task<bool> ValidateSudoMode(Session session, string? pinCode) | ||||||
|  |     { | ||||||
|  |         // Check if the session is already in sudo mode (cached) | ||||||
|  |         var sudoModeKey = $"accounts:{session.Id}:sudo"; | ||||||
|  |         var (found, _) = await cache.GetAsyncWithStatus<bool>(sudoModeKey); | ||||||
|  |          | ||||||
|  |         if (found) | ||||||
|  |         { | ||||||
|  |             // Session is already in sudo mode | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // Check if the user has a pin code | ||||||
|  |         var hasPinCode = await db.AccountAuthFactors | ||||||
|  |             .Where(f => f.AccountId == session.AccountId) | ||||||
|  |             .Where(f => f.EnabledAt != null) | ||||||
|  |             .Where(f => f.Type == AccountAuthFactorType.PinCode) | ||||||
|  |             .AnyAsync(); | ||||||
|  |              | ||||||
|  |         if (!hasPinCode) | ||||||
|  |         { | ||||||
|  |             // User doesn't have a pin code, no validation needed | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // If pin code is not provided, we can't validate | ||||||
|  |         if (string.IsNullOrEmpty(pinCode)) | ||||||
|  |         { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         try | ||||||
|  |         { | ||||||
|  |             // Validate the pin code | ||||||
|  |             var isValid = await ValidatePinCode(session.AccountId, pinCode); | ||||||
|  |              | ||||||
|  |             if (isValid) | ||||||
|  |             { | ||||||
|  |                 // Set session in sudo mode for 5 minutes | ||||||
|  |                 await cache.SetAsync(sudoModeKey, true, TimeSpan.FromMinutes(5)); | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             return isValid; | ||||||
|  |         } | ||||||
|  |         catch (InvalidOperationException) | ||||||
|  |         { | ||||||
|  |             // No pin code enabled for this account, so validation is successful | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     public async Task<bool> ValidatePinCode(Guid accountId, string pinCode) | ||||||
|  |     { | ||||||
|  |         var factor = await db.AccountAuthFactors | ||||||
|  |             .Where(f => f.AccountId == accountId) | ||||||
|  |             .Where(f => f.EnabledAt != null) | ||||||
|  |             .Where(f => f.Type == AccountAuthFactorType.PinCode) | ||||||
|  |             .FirstOrDefaultAsync(); | ||||||
|  |         if (factor is null) throw new InvalidOperationException("No pin code enabled for this account."); | ||||||
|  |  | ||||||
|  |         return factor.VerifyPassword(pinCode); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     public bool ValidateToken(string token, out Guid sessionId) |     public bool ValidateToken(string token, out Guid sessionId) | ||||||
|     { |     { | ||||||
|         sessionId = Guid.Empty; |         sessionId = Guid.Empty; | ||||||
|   | |||||||
| @@ -51,7 +51,8 @@ public class WebReaderService( | |||||||
|         var httpClient = httpClientFactory.CreateClient("WebReader"); |         var httpClient = httpClientFactory.CreateClient("WebReader"); | ||||||
|         httpClient.MaxResponseContentBufferSize = 10 * 1024 * 1024; // 10MB, prevent scrap some directly accessible files |         httpClient.MaxResponseContentBufferSize = 10 * 1024 * 1024; // 10MB, prevent scrap some directly accessible files | ||||||
|         httpClient.Timeout = TimeSpan.FromSeconds(3); |         httpClient.Timeout = TimeSpan.FromSeconds(3); | ||||||
|         httpClient.DefaultRequestHeaders.Add("User-Agent", "DysonNetwork/1.0 LinkPreview Bot"); |         // Setting UA to facebook's bot to get the opengraph. | ||||||
|  |         httpClient.DefaultRequestHeaders.Add("User-Agent", "facebookexternalhit/1.1"); | ||||||
|  |  | ||||||
|         try |         try | ||||||
|         { |         { | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| using DysonNetwork.Sphere.Account; | using DysonNetwork.Sphere.Account; | ||||||
|  | using DysonNetwork.Sphere.Localization; | ||||||
| using DysonNetwork.Sphere.Post; | using DysonNetwork.Sphere.Post; | ||||||
| using DysonNetwork.Sphere.Storage; | using DysonNetwork.Sphere.Storage; | ||||||
| using Microsoft.EntityFrameworkCore; | using Microsoft.EntityFrameworkCore; | ||||||
| @@ -10,7 +11,7 @@ public class PublisherSubscriptionService( | |||||||
|     AppDatabase db, |     AppDatabase db, | ||||||
|     NotificationService nty, |     NotificationService nty, | ||||||
|     PostService ps, |     PostService ps, | ||||||
|     IStringLocalizer<Notification> localizer, |     IStringLocalizer<NotificationResource> localizer, | ||||||
|     ICacheService cache |     ICacheService cache | ||||||
| ) | ) | ||||||
| { | { | ||||||
| @@ -49,9 +50,9 @@ public class PublisherSubscriptionService( | |||||||
|     public async Task<int> NotifySubscriberPost(Post.Post post) |     public async Task<int> NotifySubscriberPost(Post.Post post) | ||||||
|     { |     { | ||||||
|         var subscribers = await db.PublisherSubscriptions |         var subscribers = await db.PublisherSubscriptions | ||||||
|             .Include(ps => ps.Account) |             .Include(p => p.Account) | ||||||
|             .Where(ps => ps.PublisherId == post.Publisher.Id && |             .Where(p => p.PublisherId == post.PublisherId && | ||||||
|                          ps.Status == SubscriptionStatus.Active) |                          p.Status == SubscriptionStatus.Active) | ||||||
|             .ToListAsync(); |             .ToListAsync(); | ||||||
|         if (subscribers.Count == 0) |         if (subscribers.Count == 0) | ||||||
|             return 0; |             return 0; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user