diff --git a/DysonNetwork.Develop/Program.cs b/DysonNetwork.Develop/Program.cs index d653bf2..df62582 100644 --- a/DysonNetwork.Develop/Program.cs +++ b/DysonNetwork.Develop/Program.cs @@ -3,6 +3,7 @@ using DysonNetwork.Shared.Auth; using DysonNetwork.Shared.Http; using DysonNetwork.Shared.Registry; using DysonNetwork.Develop.Startup; +using DysonNetwork.Shared.Stream; using Microsoft.EntityFrameworkCore; var builder = WebApplication.CreateBuilder(args); @@ -10,6 +11,7 @@ var builder = WebApplication.CreateBuilder(args); builder.ConfigureAppKestrel(builder.Configuration); builder.Services.AddRegistryService(builder.Configuration); +builder.Services.AddStreamConnection(builder.Configuration); builder.Services.AddAppServices(builder.Configuration); builder.Services.AddAppAuthentication(); builder.Services.AddAppSwagger(); diff --git a/DysonNetwork.Pass/Account/AccountService.cs b/DysonNetwork.Pass/Account/AccountService.cs index 4959b71..704796a 100644 --- a/DysonNetwork.Pass/Account/AccountService.cs +++ b/DysonNetwork.Pass/Account/AccountService.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Text.Json; using DysonNetwork.Pass.Auth; using DysonNetwork.Pass.Auth.OpenId; using DysonNetwork.Pass.Email; @@ -6,9 +7,11 @@ using DysonNetwork.Pass.Localization; using DysonNetwork.Pass.Permission; using DysonNetwork.Shared.Cache; using DysonNetwork.Shared.Proto; +using DysonNetwork.Shared.Stream; using EFCore.BulkExtensions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Localization; +using NATS.Client.Core; using NodaTime; using OtpNet; using AuthService = DysonNetwork.Pass.Auth.AuthService; @@ -23,7 +26,8 @@ public class AccountService( PusherService.PusherServiceClient pusher, IStringLocalizer localizer, ICacheService cache, - ILogger logger + ILogger logger, + INatsConnection nats ) { public static void SetCultureInfo(Account account) @@ -183,11 +187,11 @@ public class AccountService( var dupeAutomateCount = await db.Accounts.Where(a => a.AutomatedId == automatedId).CountAsync(); if (dupeAutomateCount > 0) throw new InvalidOperationException("Automated ID has already been used."); - + var dupeNameCount = await db.Accounts.Where(a => a.Name == account.Name).CountAsync(); if (dupeNameCount > 0) throw new InvalidOperationException("Account name has already been taken."); - + account.AutomatedId = automatedId; account.ActivatedAt = SystemClock.Instance.GetCurrentInstant(); account.IsSuperuser = false; @@ -195,7 +199,7 @@ public class AccountService( await db.SaveChangesAsync(); return account; } - + public async Task GetBotAccount(Guid automatedId) { return await db.Accounts.FirstOrDefaultAsync(a => a.AutomatedId == automatedId); @@ -491,11 +495,11 @@ public class AccountService( .Where(s => s.Id == sessionId && s.AccountId == account.Id) .FirstOrDefaultAsync(); if (session is null) throw new InvalidOperationException("Session was not found."); - + // The current session should be included in the sessions' list db.AuthSessions.Remove(session); await db.SaveChangesAsync(); - + if (session.Challenge.ClientId.HasValue) { if (!await IsDeviceActive(session.Challenge.ClientId.Value)) @@ -503,7 +507,7 @@ public class AccountService( { DeviceId = session.Challenge.Client!.DeviceId } ); } - + logger.LogInformation("Deleted session #{SessionId}", session.Id); await cache.RemoveAsync($"{AuthService.AuthCachePrefix}{session.Id}"); @@ -531,7 +535,7 @@ public class AccountService( .Include(s => s.Challenge) .Where(s => s.Challenge.ClientId == device.Id) .ExecuteUpdateAsync(p => p.SetProperty(s => s.DeletedAt, s => now)); - + db.AuthClients.Remove(device); await db.SaveChangesAsync(); @@ -693,8 +697,14 @@ public class AccountService( await db.AuthSessions .Where(s => s.AccountId == account.Id) .ExecuteDeleteAsync(); - + db.Accounts.Remove(account); await db.SaveChangesAsync(); + + await nats.PublishAsync(AccountDeletedEvent.Type, JsonSerializer.SerializeToUtf8Bytes(new AccountDeletedEvent + { + AccountId = account.Id, + DeletedAt = SystemClock.Instance.GetCurrentInstant() + })); } } \ No newline at end of file diff --git a/DysonNetwork.Shared/Stream/AccountEvent.cs b/DysonNetwork.Shared/Stream/AccountEvent.cs index dec0b18..016f29f 100644 --- a/DysonNetwork.Shared/Stream/AccountEvent.cs +++ b/DysonNetwork.Shared/Stream/AccountEvent.cs @@ -4,6 +4,8 @@ namespace DysonNetwork.Shared.Stream; public class AccountDeletedEvent { + public static string Type => "account.deleted"; + public Guid AccountId { get; set; } = Guid.NewGuid(); public Instant DeletedAt { get; set; } = SystemClock.Instance.GetCurrentInstant(); } \ No newline at end of file diff --git a/DysonNetwork.Sphere/Program.cs b/DysonNetwork.Sphere/Program.cs index c221ed8..1087936 100644 --- a/DysonNetwork.Sphere/Program.cs +++ b/DysonNetwork.Sphere/Program.cs @@ -2,6 +2,7 @@ using DysonNetwork.Shared.Auth; using DysonNetwork.Shared.Http; using DysonNetwork.Shared.PageData; using DysonNetwork.Shared.Registry; +using DysonNetwork.Shared.Stream; using DysonNetwork.Sphere; using DysonNetwork.Sphere.PageData; using DysonNetwork.Sphere.Startup; @@ -18,6 +19,7 @@ builder.Services.AddAppMetrics(); // Add application services builder.Services.AddRegistryService(builder.Configuration); +builder.Services.AddStreamConnection(builder.Configuration); builder.Services.AddAppServices(builder.Configuration); builder.Services.AddAppRateLimiting(); builder.Services.AddAppAuthentication(); diff --git a/DysonNetwork.Sphere/Startup/BroadcastEventHandler.cs b/DysonNetwork.Sphere/Startup/BroadcastEventHandler.cs new file mode 100644 index 0000000..b2473e6 --- /dev/null +++ b/DysonNetwork.Sphere/Startup/BroadcastEventHandler.cs @@ -0,0 +1,51 @@ +using System.Text.Json; +using DysonNetwork.Shared.Stream; +using Microsoft.EntityFrameworkCore; +using NATS.Client.Core; + +namespace DysonNetwork.Sphere.Startup; + +public class BroadcastEventHandler( + INatsConnection nats, + ILogger logger, + AppDatabase db +) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await foreach (var msg in nats.SubscribeAsync("accounts.deleted", cancellationToken: stoppingToken)) + { + try + { + var evt = JsonSerializer.Deserialize(msg.Data); + if (evt == null) continue; + + logger.LogInformation("Account deleted: {AccountId}", evt.AccountId); + + await using var transaction = await db.Database.BeginTransactionAsync(cancellationToken: stoppingToken); + try + { + var publishers = await db.Publishers + .Where(p => p.Members.All(m => m.AccountId == evt.AccountId)) + .ToListAsync(cancellationToken: stoppingToken); + + foreach (var publisher in publishers) + await db.Posts + .Where(p => p.PublisherId == publisher.Id) + .ExecuteDeleteAsync(cancellationToken: stoppingToken); + + await transaction.CommitAsync(cancellationToken: stoppingToken); + } + catch (Exception) + { + await transaction.RollbackAsync(cancellationToken: stoppingToken); + throw; + } + } + catch (Exception ex) + { + logger.LogError(ex, "Error processing AccountDeleted"); + } + } + } +} \ No newline at end of file diff --git a/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs b/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs index cdb318f..efb7335 100644 --- a/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs +++ b/DysonNetwork.Sphere/Startup/ServiceCollectionExtensions.cs @@ -73,6 +73,8 @@ public static class ServiceCollectionExtensions options.SupportedUICultures = supportedCultures; }); + services.AddHostedService(); + return services; } diff --git a/DysonNetwork.sln.DotSettings.user b/DysonNetwork.sln.DotSettings.user index 8b151a9..f8a57a7 100644 --- a/DysonNetwork.sln.DotSettings.user +++ b/DysonNetwork.sln.DotSettings.user @@ -71,6 +71,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded