♻️ Rebuilt own auth infra
This commit is contained in:
		| @@ -20,28 +20,32 @@ public class ActionLogService(AppDatabase db, GeoIpService geo) : IDisposable | ||||
|  | ||||
|         _creationQueue.Enqueue(log); | ||||
|     } | ||||
|      | ||||
|     public void CreateActionLogFromRequest(string action, Dictionary<string, object> meta, HttpRequest request) | ||||
|  | ||||
|     public void CreateActionLogFromRequest(string action, Dictionary<string, object> meta, HttpRequest request, | ||||
|         Account? account = null) | ||||
|     { | ||||
|         if (request.HttpContext.Items["CurrentUser"] is not Account currentUser) | ||||
|             throw new ArgumentException("No user context was found"); | ||||
|         if (request.HttpContext.Items["CurrentSession"] is not Auth.Session currentSession) | ||||
|             throw new ArgumentException("No session context was found"); | ||||
|          | ||||
|         var log = new ActionLog | ||||
|         { | ||||
|             Action = action, | ||||
|             AccountId = currentUser.Id, | ||||
|             SessionId = currentSession.Id, | ||||
|             Meta = meta, | ||||
|             UserAgent = request.Headers.UserAgent, | ||||
|             IpAddress = request.HttpContext.Connection.RemoteIpAddress?.ToString(), | ||||
|             Location = geo.GetPointFromIp(request.HttpContext.Connection.RemoteIpAddress?.ToString()) | ||||
|         }; | ||||
|      | ||||
|          | ||||
|         if (request.HttpContext.Items["CurrentUser"] is Account currentUser) | ||||
|             log.AccountId = currentUser.Id; | ||||
|         else if (account != null) | ||||
|             log.AccountId = account.Id; | ||||
|         else | ||||
|             throw new ArgumentException("No user context was found"); | ||||
|          | ||||
|         if (request.HttpContext.Items["CurrentSession"] is Auth.Session currentSession) | ||||
|             log.SessionId = currentSession.Id; | ||||
|  | ||||
|         _creationQueue.Enqueue(log); | ||||
|     } | ||||
|      | ||||
|  | ||||
|     public async Task FlushQueue() | ||||
|     { | ||||
|         var workingQueue = new List<ActionLog>(); | ||||
|   | ||||
| @@ -17,11 +17,14 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) : | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|         var userId = currentUser.Id; | ||||
|  | ||||
|         var totalCount = await db.AccountRelationships | ||||
|             .CountAsync(r => r.Account.Id == userId); | ||||
|         var relationships = await db.AccountRelationships | ||||
|             .Where(r => r.Account.Id == userId) | ||||
|         var query = db.AccountRelationships.AsQueryable() | ||||
|             .Where(r => r.RelatedId == userId); | ||||
|         var totalCount = await query.CountAsync(); | ||||
|         var relationships = await query | ||||
|             .Include(r => r.Related) | ||||
|             .Include(r => r.Related.Profile) | ||||
|             .Include(r => r.Account) | ||||
|             .Include(r => r.Account.Profile) | ||||
|             .Skip(offset) | ||||
|             .Take(take) | ||||
|             .ToListAsync(); | ||||
| @@ -30,21 +33,37 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) : | ||||
|  | ||||
|         return relationships; | ||||
|     } | ||||
|  | ||||
|     public class RelationshipCreateRequest | ||||
|      | ||||
|     [HttpGet("requests")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<List<Relationship>>> ListSentRequests() | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|      | ||||
|         var relationships = await db.AccountRelationships | ||||
|             .Where(r => r.AccountId == currentUser.Id && r.Status == RelationshipStatus.Pending) | ||||
|             .Include(r => r.Related) | ||||
|             .Include(r => r.Related.Profile) | ||||
|             .Include(r => r.Account) | ||||
|             .Include(r => r.Account.Profile) | ||||
|             .ToListAsync(); | ||||
|      | ||||
|         return relationships; | ||||
|     } | ||||
|  | ||||
|     public class RelationshipRequest | ||||
|     { | ||||
|         [Required] public long UserId { get; set; } | ||||
|         [Required] public RelationshipStatus Status { get; set; } | ||||
|     } | ||||
|  | ||||
|     [HttpPost] | ||||
|     [HttpPost("{userId:guid}")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<Relationship>> CreateRelationship([FromBody] RelationshipCreateRequest request) | ||||
|     public async Task<ActionResult<Relationship>> CreateRelationship(Guid userId, [FromBody] RelationshipRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|          | ||||
|         var relatedUser = await db.Accounts.FindAsync(request.UserId); | ||||
|         if (relatedUser is null) return BadRequest("Invalid related user"); | ||||
|  | ||||
|         var relatedUser = await db.Accounts.FindAsync(userId); | ||||
|         if (relatedUser is null) return NotFound("Account was not found."); | ||||
|  | ||||
|         try | ||||
|         { | ||||
| @@ -58,4 +77,105 @@ public class RelationshipController(AppDatabase db, RelationshipService rels) : | ||||
|             return BadRequest(err.Message); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     [HttpPatch("{userId:guid}")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<Relationship>> UpdateRelationship(Guid userId, [FromBody] RelationshipRequest request) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|      | ||||
|         try | ||||
|         { | ||||
|             var relationship = await rels.UpdateRelationship(currentUser.Id, userId, request.Status); | ||||
|             return relationship; | ||||
|         } | ||||
|         catch (ArgumentException err) | ||||
|         { | ||||
|             return NotFound(err.Message); | ||||
|         } | ||||
|         catch (InvalidOperationException err) | ||||
|         { | ||||
|             return BadRequest(err.Message); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     [HttpPost("{userId:guid}/friends")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<Relationship>> SendFriendRequest(Guid userId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         var relatedUser = await db.Accounts.FindAsync(userId); | ||||
|         if (relatedUser is null) return NotFound("Account was not found."); | ||||
|  | ||||
|         try | ||||
|         { | ||||
|             var relationship = await rels.SendFriendRequest(currentUser, relatedUser); | ||||
|             return relationship; | ||||
|         } | ||||
|         catch (InvalidOperationException err) | ||||
|         { | ||||
|             return BadRequest(err.Message); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     [HttpPost("{userId:guid}/friends/accept")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<Relationship>> AcceptFriendRequest(Guid userId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         var relationship = await rels.GetRelationship(userId, currentUser.Id, RelationshipStatus.Pending); | ||||
|         if (relationship is null) return NotFound("Friend request was not found."); | ||||
|          | ||||
|         try | ||||
|         { | ||||
|             relationship = await rels.AcceptFriendRelationship(relationship); | ||||
|             return relationship; | ||||
|         } | ||||
|         catch (InvalidOperationException err) | ||||
|         { | ||||
|             return BadRequest(err.Message); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     [HttpPost("{userId:guid}/friends/decline")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<Relationship>> DeclineFriendRequest(Guid userId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|  | ||||
|         var relationship = await rels.GetRelationship(userId, currentUser.Id, RelationshipStatus.Pending); | ||||
|         if (relationship is null) return NotFound("Friend request was not found."); | ||||
|          | ||||
|         try | ||||
|         { | ||||
|             relationship = await rels.AcceptFriendRelationship(relationship, status: RelationshipStatus.Blocked); | ||||
|             return relationship; | ||||
|         } | ||||
|         catch (InvalidOperationException err) | ||||
|         { | ||||
|             return BadRequest(err.Message); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     [HttpPost("{userId:guid}/block")] | ||||
|     [Authorize] | ||||
|     public async Task<ActionResult<Relationship>> BlockUser(Guid userId) | ||||
|     { | ||||
|         if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); | ||||
|      | ||||
|         var relatedUser = await db.Accounts.FindAsync(userId); | ||||
|         if (relatedUser is null) return NotFound("Account was not found."); | ||||
|      | ||||
|         try | ||||
|         { | ||||
|             var relationship = await rels.BlockAccount(currentUser, relatedUser); | ||||
|             return relationship; | ||||
|         } | ||||
|         catch (InvalidOperationException err) | ||||
|         { | ||||
|             return BadRequest(err.Message); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,32 +1,31 @@ | ||||
| using DysonNetwork.Sphere.Permission; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.Extensions.Caching.Memory; | ||||
| using NodaTime; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Account; | ||||
|  | ||||
| public class RelationshipService(AppDatabase db, PermissionService pm, IMemoryCache cache) | ||||
| public class RelationshipService(AppDatabase db, IMemoryCache cache) | ||||
| { | ||||
|     public async Task<bool> HasExistingRelationship(Account userA, Account userB) | ||||
|     public async Task<bool> HasExistingRelationship(Guid accountId, Guid relatedId) | ||||
|     { | ||||
|         var count = await db.AccountRelationships | ||||
|             .Where(r => (r.AccountId == userA.Id && r.AccountId == userB.Id) || | ||||
|                         (r.AccountId == userB.Id && r.AccountId == userA.Id)) | ||||
|             .Where(r => (r.AccountId == accountId && r.RelatedId == relatedId) || | ||||
|                         (r.AccountId == relatedId && r.AccountId == accountId)) | ||||
|             .CountAsync(); | ||||
|         return count > 0; | ||||
|     } | ||||
|  | ||||
|     public async Task<Relationship?> GetRelationship( | ||||
|         Account account, | ||||
|         Account related, | ||||
|         RelationshipStatus? status, | ||||
|         Guid accountId, | ||||
|         Guid relatedId, | ||||
|         RelationshipStatus? status = null, | ||||
|         bool ignoreExpired = false | ||||
|     ) | ||||
|     { | ||||
|         var now = Instant.FromDateTimeUtc(DateTime.UtcNow); | ||||
|         var queries = db.AccountRelationships | ||||
|             .Where(r => r.AccountId == account.Id && r.AccountId == related.Id); | ||||
|         if (ignoreExpired) queries = queries.Where(r => r.ExpiredAt > now); | ||||
|         var queries = db.AccountRelationships.AsQueryable() | ||||
|             .Where(r => r.AccountId == accountId && r.RelatedId == relatedId); | ||||
|         if (!ignoreExpired) queries = queries.Where(r => r.ExpiredAt > now); | ||||
|         if (status is not null) queries = queries.Where(r => r.Status == status); | ||||
|         var relationship = await queries.FirstOrDefaultAsync(); | ||||
|         return relationship; | ||||
| @@ -37,7 +36,7 @@ public class RelationshipService(AppDatabase db, PermissionService pm, IMemoryCa | ||||
|         if (status == RelationshipStatus.Pending) | ||||
|             throw new InvalidOperationException( | ||||
|                 "Cannot create relationship with pending status, use SendFriendRequest instead."); | ||||
|         if (await HasExistingRelationship(sender, target)) | ||||
|         if (await HasExistingRelationship(sender.Id, target.Id)) | ||||
|             throw new InvalidOperationException("Found existing relationship between you and target user."); | ||||
|  | ||||
|         var relationship = new Relationship | ||||
| @@ -49,17 +48,23 @@ public class RelationshipService(AppDatabase db, PermissionService pm, IMemoryCa | ||||
|  | ||||
|         db.AccountRelationships.Add(relationship); | ||||
|         await db.SaveChangesAsync(); | ||||
|         await ApplyRelationshipPermissions(relationship); | ||||
|          | ||||
|  | ||||
|         cache.Remove($"UserFriends_{relationship.AccountId}"); | ||||
|         cache.Remove($"UserFriends_{relationship.RelatedId}"); | ||||
|  | ||||
|         return relationship; | ||||
|     } | ||||
|  | ||||
|     public async Task<Relationship> BlockAccount(Account sender, Account target) | ||||
|     { | ||||
|         if (await HasExistingRelationship(sender.Id, target.Id)) | ||||
|             return await UpdateRelationship(sender.Id, target.Id, RelationshipStatus.Blocked); | ||||
|         return await CreateRelationship(sender, target, RelationshipStatus.Blocked); | ||||
|     } | ||||
|  | ||||
|     public async Task<Relationship> SendFriendRequest(Account sender, Account target) | ||||
|     { | ||||
|         if (await HasExistingRelationship(sender, target)) | ||||
|         if (await HasExistingRelationship(sender.Id, target.Id)) | ||||
|             throw new InvalidOperationException("Found existing relationship between you and target user."); | ||||
|  | ||||
|         var relationship = new Relationship | ||||
| @@ -81,7 +86,9 @@ public class RelationshipService(AppDatabase db, PermissionService pm, IMemoryCa | ||||
|         RelationshipStatus status = RelationshipStatus.Friends | ||||
|     ) | ||||
|     { | ||||
|         if (relationship.Status == RelationshipStatus.Pending) | ||||
|         if (relationship.Status != RelationshipStatus.Pending) | ||||
|             throw new ArgumentException("Cannot accept friend request that not in pending status."); | ||||
|         if (status == RelationshipStatus.Pending) | ||||
|             throw new ArgumentException("Cannot accept friend request by setting the new status to pending."); | ||||
|  | ||||
|         // Whatever the receiver decides to apply which status to the relationship, | ||||
| @@ -100,27 +107,22 @@ public class RelationshipService(AppDatabase db, PermissionService pm, IMemoryCa | ||||
|  | ||||
|         await db.SaveChangesAsync(); | ||||
|  | ||||
|         await Task.WhenAll( | ||||
|             ApplyRelationshipPermissions(relationship), | ||||
|             ApplyRelationshipPermissions(relationshipBackward) | ||||
|         ); | ||||
|          | ||||
|         cache.Remove($"UserFriends_{relationship.AccountId}"); | ||||
|         cache.Remove($"UserFriends_{relationship.RelatedId}"); | ||||
|  | ||||
|         return relationshipBackward; | ||||
|     } | ||||
|  | ||||
|     public async Task<Relationship> UpdateRelationship(Account account, Account related, RelationshipStatus status) | ||||
|     public async Task<Relationship> UpdateRelationship(Guid accountId, Guid relatedId, RelationshipStatus status) | ||||
|     { | ||||
|         var relationship = await GetRelationship(account, related, status); | ||||
|         var relationship = await GetRelationship(accountId, relatedId, status); | ||||
|         if (relationship is null) throw new ArgumentException("There is no relationship between you and the user."); | ||||
|         if (relationship.Status == status) return relationship; | ||||
|         relationship.Status = status; | ||||
|         db.Update(relationship); | ||||
|         await db.SaveChangesAsync(); | ||||
|         await ApplyRelationshipPermissions(relationship); | ||||
|         cache.Remove($"UserFriends_{related.Id}"); | ||||
|         cache.Remove($"UserFriends_{accountId}"); | ||||
|         cache.Remove($"UserFriends_{relatedId}"); | ||||
|         return relationship; | ||||
|     } | ||||
|  | ||||
| @@ -139,27 +141,10 @@ public class RelationshipService(AppDatabase db, PermissionService pm, IMemoryCa | ||||
|         return friends ?? []; | ||||
|     } | ||||
|  | ||||
|     private async Task ApplyRelationshipPermissions(Relationship relationship) | ||||
|     public async Task<bool> HasRelationshipWithStatus(Guid accountId, Guid relatedId, | ||||
|         RelationshipStatus status = RelationshipStatus.Friends) | ||||
|     { | ||||
|         // Apply the relationship permissions to casbin enforcer | ||||
|         // domain: the user | ||||
|         // status is friends: all permissions are allowed by default, expect specially specified | ||||
|         // status is blocked: all permissions are disallowed by default, expect specially specified | ||||
|         // others: use the default permissions by design | ||||
|  | ||||
|         var domain = $"user:{relationship.AccountId.ToString()}"; | ||||
|         var target = $"user:{relationship.RelatedId.ToString()}"; | ||||
|  | ||||
|         await pm.RemovePermissionNode(target, domain, "*"); | ||||
|  | ||||
|         bool? value = relationship.Status switch | ||||
|         { | ||||
|             RelationshipStatus.Friends => true, | ||||
|             RelationshipStatus.Blocked => false, | ||||
|             _ => null, | ||||
|         }; | ||||
|         if (value is null) return; | ||||
|  | ||||
|         await pm.AddPermissionNode(target, domain, "*", value); | ||||
|         var relationship = await GetRelationship(accountId, relatedId, status); | ||||
|         return relationship is not null; | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user