♻️ Rebuilt own auth infra

This commit is contained in:
2025-05-16 23:38:33 +08:00
parent aabe8269f5
commit 88977ccda3
10 changed files with 309 additions and 126 deletions

View File

@ -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>();

View File

@ -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);
}
}
}

View File

@ -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;
}
}