From 344ed6e348f965523f7affbc4c8012d55bbce073 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 30 Nov 2025 17:16:11 +0800 Subject: [PATCH] :sparkles: Account validation endpoint --- .../Account/AccountController.cs | 42 ++++++++++++++++++- DysonNetwork.Pass/Account/AccountService.cs | 28 +++++++++---- 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/DysonNetwork.Pass/Account/AccountController.cs b/DysonNetwork.Pass/Account/AccountController.cs index 63b638f..4e69eae 100644 --- a/DysonNetwork.Pass/Account/AccountController.cs +++ b/DysonNetwork.Pass/Account/AccountController.cs @@ -34,7 +34,7 @@ public class AccountController( .Include(e => e.Badges) .Include(e => e.Profile) .Include(e => e.Contacts.Where(c => c.IsPublic)) - .Where(a => a.Name == name) + .Where(a => EF.Functions.Like(a.Name, name)) .FirstOrDefaultAsync(); if (account is null) return NotFound(ApiError.NotFound(name, traceId: HttpContext.TraceIdentifier)); @@ -105,6 +105,44 @@ public class AccountController( [Required] public string CaptchaToken { get; set; } = string.Empty; } + public class AccountCreateValidateRequest + { + [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; } + + [EmailAddress] + [RegularExpression(@"^[^+]+@[^@]+\.[^@]+$", ErrorMessage = "Email address cannot contain '+' symbol.")] + [Required] + [MaxLength(1024)] + public string? Email { get; set; } + } + + [HttpPost("validate")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> ValidateCreateAccountRequest( + [FromBody] AccountCreateValidateRequest request) + { + if (request.Name is not null) + { + if (await accounts.CheckAccountNameHasTaken(request.Name)) + return BadRequest("Account name has already been taken."); + } + + if (request.Email is not null) + { + if (await accounts.CheckEmailHasBeenUsed(request.Email)) + return BadRequest("Email has already been used."); + } + + return Ok("Everything seems good."); + } + [HttpPost] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -277,4 +315,4 @@ public class AccountController( await socialCreditService.ValidateSocialCredits(); return Ok(); } -} +} \ No newline at end of file diff --git a/DysonNetwork.Pass/Account/AccountService.cs b/DysonNetwork.Pass/Account/AccountService.cs index f29c535..0b8e05a 100644 --- a/DysonNetwork.Pass/Account/AccountService.cs +++ b/DysonNetwork.Pass/Account/AccountService.cs @@ -54,7 +54,7 @@ public class AccountService( public async Task LookupAccount(string probe) { - var account = await db.Accounts.Where(a => a.Name == probe).FirstOrDefaultAsync(); + var account = await db.Accounts.Where(a => EF.Functions.ILike(a.Name, probe)).FirstOrDefaultAsync(); if (account is not null) return account; var contact = await db.AccountContacts @@ -81,6 +81,17 @@ public class AccountService( return profile?.Level; } + public async Task CheckAccountNameHasTaken(string name) + { + return await db.Accounts.AnyAsync(a => EF.Functions.ILike(a.Name, name)); + } + + public async Task CheckEmailHasBeenUsed(string email) + { + return await db.AccountContacts.AnyAsync(c => + c.Type == Shared.Models.AccountContactType.Email && EF.Functions.ILike(c.Content, email)); + } + public async Task CreateAccount( string name, string nick, @@ -92,8 +103,7 @@ public class AccountService( bool isActivated = false ) { - var dupeNameCount = await db.Accounts.Where(a => a.Name == name).CountAsync(); - if (dupeNameCount > 0) + if (await CheckAccountNameHasTaken(name)) throw new InvalidOperationException("Account name has already been taken."); var dupeEmailCount = await db.AccountContacts @@ -274,7 +284,8 @@ public class AccountService( return isExists; } - public async Task CreateAuthFactor(SnAccount account, Shared.Models.AccountAuthFactorType type, string? secret) + public async Task CreateAuthFactor(SnAccount account, + Shared.Models.AccountAuthFactorType type, string? secret) { SnAccountAuthFactor? factor = null; switch (type) @@ -352,7 +363,8 @@ public class AccountService( public async Task EnableAuthFactor(SnAccountAuthFactor factor, string? code) { if (factor.EnabledAt is not null) throw new ArgumentException("The factor has been enabled."); - if (factor.Type is Shared.Models.AccountAuthFactorType.Password or Shared.Models.AccountAuthFactorType.TimedCode) + if (factor.Type is Shared.Models.AccountAuthFactorType.Password + or Shared.Models.AccountAuthFactorType.TimedCode) { if (code is null || !factor.VerifyPassword(code)) throw new InvalidOperationException( @@ -577,7 +589,8 @@ public class AccountService( await cache.RemoveAsync($"{AuthService.AuthCachePrefix}{item.Id}"); } - public async Task CreateContactMethod(SnAccount account, Shared.Models.AccountContactType type, string content) + public async Task CreateContactMethod(SnAccount account, Shared.Models.AccountContactType type, + string content) { var isExists = await db.AccountContacts .Where(x => x.AccountId == account.Id && x.Type == type && x.Content == content) @@ -639,7 +652,8 @@ public class AccountService( } } - public async Task SetContactMethodPublic(SnAccount account, SnAccountContact contact, bool isPublic) + public async Task SetContactMethodPublic(SnAccount account, SnAccountContact contact, + bool isPublic) { contact.IsPublic = isPublic; db.AccountContacts.Update(contact);