From b1c12685c81daef5976844a7b75464392ea0b4c4 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 7 Jun 2025 16:35:22 +0800 Subject: [PATCH] :sparkles: Last seen at :necktie: Update register account validation --- .../Account/AccountController.cs | 10 ++- DysonNetwork.Sphere/Auth/Auth.cs | 13 +++- DysonNetwork.Sphere/Auth/Session.cs | 2 +- DysonNetwork.Sphere/Program.cs | 3 +- .../Handlers/LastActiveFlushHandler.cs | 69 +++++++++++++++++++ 5 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 DysonNetwork.Sphere/Storage/Handlers/LastActiveFlushHandler.cs diff --git a/DysonNetwork.Sphere/Account/AccountController.cs b/DysonNetwork.Sphere/Account/AccountController.cs index c3d68f0..7c1b2ad 100644 --- a/DysonNetwork.Sphere/Account/AccountController.cs +++ b/DysonNetwork.Sphere/Account/AccountController.cs @@ -50,10 +50,18 @@ public class AccountController( public class AccountCreateRequest { - [Required] [MaxLength(256)] public string Name { get; set; } = string.Empty; + [Required] + [MinLength(2)] + [MaxLength(256)] + [RegularExpression(@"^[A-Za-z0-9_-]+$", + ErrorMessage = "Name can only contain letters, numbers, underscores, and hyphens.") + ] + public string Name { get; set; } = string.Empty; + [Required] [MaxLength(256)] public string Nick { get; set; } = string.Empty; [EmailAddress] + [RegularExpression(@"^[^+]+@[^@]+\.[^@]+$", ErrorMessage = "Email address cannot contain '+' symbol.")] [Required] [MaxLength(1024)] public string Email { get; set; } = string.Empty; diff --git a/DysonNetwork.Sphere/Auth/Auth.cs b/DysonNetwork.Sphere/Auth/Auth.cs index 3919b07..fe47dbb 100644 --- a/DysonNetwork.Sphere/Auth/Auth.cs +++ b/DysonNetwork.Sphere/Auth/Auth.cs @@ -3,6 +3,7 @@ using System.Security.Cryptography; using System.Text.Encodings.Web; using DysonNetwork.Sphere.Account; using DysonNetwork.Sphere.Storage; +using DysonNetwork.Sphere.Storage.Handlers; using Microsoft.AspNetCore.Authentication; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; @@ -37,7 +38,8 @@ public class DysonTokenAuthHandler( ILoggerFactory logger, UrlEncoder encoder, AppDatabase database, - ICacheService cache + ICacheService cache, + FlushBufferService fbs ) : AuthenticationHandler(options, logger, encoder) { @@ -116,6 +118,15 @@ public class DysonTokenAuthHandler( var principal = new ClaimsPrincipal(identity); var ticket = new AuthenticationTicket(principal, AuthConstants.SchemeName); + + var lastInfo = new LastActiveInfo + { + Account = session.Account, + Session = session, + SeenAt = SystemClock.Instance.GetCurrentInstant(), + }; + fbs.Enqueue(lastInfo); + return AuthenticateResult.Success(ticket); } catch (Exception ex) diff --git a/DysonNetwork.Sphere/Auth/Session.cs b/DysonNetwork.Sphere/Auth/Session.cs index d33a95f..dcd8f33 100644 --- a/DysonNetwork.Sphere/Auth/Session.cs +++ b/DysonNetwork.Sphere/Auth/Session.cs @@ -16,7 +16,7 @@ public class Session : ModelBase public Guid AccountId { get; set; } [JsonIgnore] public Account.Account Account { get; set; } = null!; public Guid ChallengeId { get; set; } - [JsonIgnore] public Challenge Challenge { get; set; } = null!; + public Challenge Challenge { get; set; } = null!; } public enum ChallengeType diff --git a/DysonNetwork.Sphere/Program.cs b/DysonNetwork.Sphere/Program.cs index 94999be..af6c2c0 100644 --- a/DysonNetwork.Sphere/Program.cs +++ b/DysonNetwork.Sphere/Program.cs @@ -185,7 +185,7 @@ builder.Services.AddSingleton(tusDiskStore); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); // The handlers for websocket builder.Services.AddScoped(); @@ -199,6 +199,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/DysonNetwork.Sphere/Storage/Handlers/LastActiveFlushHandler.cs b/DysonNetwork.Sphere/Storage/Handlers/LastActiveFlushHandler.cs new file mode 100644 index 0000000..717705a --- /dev/null +++ b/DysonNetwork.Sphere/Storage/Handlers/LastActiveFlushHandler.cs @@ -0,0 +1,69 @@ +using EFCore.BulkExtensions; +using Microsoft.EntityFrameworkCore; +using NodaTime; +using Quartz; + +namespace DysonNetwork.Sphere.Storage.Handlers; + +public class LastActiveInfo +{ + public Auth.Session Session { get; set; } + public Account.Account Account { get; set; } + public Instant SeenAt { get; set; } +} + +public class LastActiveFlushHandler(IServiceProvider serviceProvider) : IFlushHandler +{ + public async Task FlushAsync(IReadOnlyList items) + { + using var scope = serviceProvider.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + + // Remove duplicates by grouping on (sessionId, accountId), taking the most recent SeenAt + var distinctItems = items + .GroupBy(x => (SessionId: x.Session.Id, AccountId: x.Account.Id)) + .Select(g => g.OrderByDescending(x => x.SeenAt).First()) + .ToList(); + + // Build dictionaries so we can match session/account IDs to their new "last seen" timestamps + var sessionIdMap = distinctItems + .GroupBy(x => x.Session.Id) + .ToDictionary(g => g.Key, g => g.Last().SeenAt); + + var accountIdMap = distinctItems + .GroupBy(x => x.Account.Id) + .ToDictionary(g => g.Key, g => g.Last().SeenAt); + + // Load all sessions that need to be updated in one batch + var sessionsToUpdate = await db.AuthSessions + .Where(s => sessionIdMap.Keys.Contains(s.Id)) + .ToListAsync(); + + // Update their LastGrantedAt + foreach (var session in sessionsToUpdate) + session.LastGrantedAt = sessionIdMap[session.Id]; + + // Bulk update sessions + await db.BulkUpdateAsync(sessionsToUpdate); + + // Similarly, load account profiles in one batch + var accountProfilesToUpdate = await db.AccountProfiles + .Where(a => accountIdMap.Keys.Contains(a.AccountId)) + .ToListAsync(); + + // Update their LastSeenAt + foreach (var profile in accountProfilesToUpdate) + profile.LastSeenAt = accountIdMap[profile.AccountId]; + + // Bulk update profiles + await db.BulkUpdateAsync(accountProfilesToUpdate); + } +} + +public class LastActiveFlushJob(FlushBufferService fbs, ActionLogFlushHandler hdl) : IJob +{ + public async Task Execute(IJobExecutionContext context) + { + await fbs.FlushAsync(hdl); + } +} \ No newline at end of file