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 AccountAuthFactorType Type { 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> | ||||
|     /// The trustworthy stands for how safe is this auth factor. | ||||
|     /// 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> | ||||
|     public int Trustworthy { get; set; } = 1; | ||||
|  | ||||
| @@ -148,6 +151,7 @@ public class AccountAuthFactor : ModelBase | ||||
|         switch (Type) | ||||
|         { | ||||
|             case AccountAuthFactorType.Password: | ||||
|             case AccountAuthFactorType.PinCode: | ||||
|                 return BCrypt.Net.BCrypt.Verify(password, Secret); | ||||
|             case AccountAuthFactorType.TimedCode: | ||||
|                 var otp = new Totp(Base32Encoding.ToBytes(Secret)); | ||||
| @@ -172,7 +176,8 @@ public enum AccountAuthFactorType | ||||
|     Password, | ||||
|     EmailCode, | ||||
|     InAppCode, | ||||
|     TimedCode | ||||
|     TimedCode, | ||||
|     PinCode, | ||||
| } | ||||
|  | ||||
| public class AccountConnection : ModelBase | ||||
|   | ||||
| @@ -257,6 +257,18 @@ public class AccountService( | ||||
|                     } | ||||
|                 }; | ||||
|                 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: | ||||
|                 throw new ArgumentOutOfRangeException(nameof(type), type, null); | ||||
|         } | ||||
|   | ||||
| @@ -1,11 +1,19 @@ | ||||
| using System.Security.Cryptography; | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Sphere.Account; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
|  | ||||
| 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!; | ||||
|  | ||||
| @@ -174,6 +182,69 @@ public class AuthService(AppDatabase db, IConfiguration config, IHttpClientFacto | ||||
|         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) | ||||
|     { | ||||
|         sessionId = Guid.Empty; | ||||
|   | ||||
| @@ -51,7 +51,8 @@ public class WebReaderService( | ||||
|         var httpClient = httpClientFactory.CreateClient("WebReader"); | ||||
|         httpClient.MaxResponseContentBufferSize = 10 * 1024 * 1024; // 10MB, prevent scrap some directly accessible files | ||||
|         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 | ||||
|         { | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| using DysonNetwork.Sphere.Account; | ||||
| using DysonNetwork.Sphere.Localization; | ||||
| using DysonNetwork.Sphere.Post; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| @@ -10,7 +11,7 @@ public class PublisherSubscriptionService( | ||||
|     AppDatabase db, | ||||
|     NotificationService nty, | ||||
|     PostService ps, | ||||
|     IStringLocalizer<Notification> localizer, | ||||
|     IStringLocalizer<NotificationResource> localizer, | ||||
|     ICacheService cache | ||||
| ) | ||||
| { | ||||
| @@ -49,9 +50,9 @@ public class PublisherSubscriptionService( | ||||
|     public async Task<int> NotifySubscriberPost(Post.Post post) | ||||
|     { | ||||
|         var subscribers = await db.PublisherSubscriptions | ||||
|             .Include(ps => ps.Account) | ||||
|             .Where(ps => ps.PublisherId == post.Publisher.Id && | ||||
|                          ps.Status == SubscriptionStatus.Active) | ||||
|             .Include(p => p.Account) | ||||
|             .Where(p => p.PublisherId == post.PublisherId && | ||||
|                          p.Status == SubscriptionStatus.Active) | ||||
|             .ToListAsync(); | ||||
|         if (subscribers.Count == 0) | ||||
|             return 0; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user