✨ Account validation endpoint
This commit is contained in:
@@ -34,7 +34,7 @@ public class AccountController(
|
|||||||
.Include(e => e.Badges)
|
.Include(e => e.Badges)
|
||||||
.Include(e => e.Profile)
|
.Include(e => e.Profile)
|
||||||
.Include(e => e.Contacts.Where(c => c.IsPublic))
|
.Include(e => e.Contacts.Where(c => c.IsPublic))
|
||||||
.Where(a => a.Name == name)
|
.Where(a => EF.Functions.Like(a.Name, name))
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
if (account is null) return NotFound(ApiError.NotFound(name, traceId: HttpContext.TraceIdentifier));
|
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;
|
[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<ActionResult<string>> 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]
|
[HttpPost]
|
||||||
[ProducesResponseType<SnAccount>(StatusCodes.Status200OK)]
|
[ProducesResponseType<SnAccount>(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ public class AccountService(
|
|||||||
|
|
||||||
public async Task<SnAccount?> LookupAccount(string probe)
|
public async Task<SnAccount?> 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;
|
if (account is not null) return account;
|
||||||
|
|
||||||
var contact = await db.AccountContacts
|
var contact = await db.AccountContacts
|
||||||
@@ -81,6 +81,17 @@ public class AccountService(
|
|||||||
return profile?.Level;
|
return profile?.Level;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CheckAccountNameHasTaken(string name)
|
||||||
|
{
|
||||||
|
return await db.Accounts.AnyAsync(a => EF.Functions.ILike(a.Name, name));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> CheckEmailHasBeenUsed(string email)
|
||||||
|
{
|
||||||
|
return await db.AccountContacts.AnyAsync(c =>
|
||||||
|
c.Type == Shared.Models.AccountContactType.Email && EF.Functions.ILike(c.Content, email));
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<SnAccount> CreateAccount(
|
public async Task<SnAccount> CreateAccount(
|
||||||
string name,
|
string name,
|
||||||
string nick,
|
string nick,
|
||||||
@@ -92,8 +103,7 @@ public class AccountService(
|
|||||||
bool isActivated = false
|
bool isActivated = false
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var dupeNameCount = await db.Accounts.Where(a => a.Name == name).CountAsync();
|
if (await CheckAccountNameHasTaken(name))
|
||||||
if (dupeNameCount > 0)
|
|
||||||
throw new InvalidOperationException("Account name has already been taken.");
|
throw new InvalidOperationException("Account name has already been taken.");
|
||||||
|
|
||||||
var dupeEmailCount = await db.AccountContacts
|
var dupeEmailCount = await db.AccountContacts
|
||||||
@@ -274,7 +284,8 @@ public class AccountService(
|
|||||||
return isExists;
|
return isExists;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SnAccountAuthFactor?> CreateAuthFactor(SnAccount account, Shared.Models.AccountAuthFactorType type, string? secret)
|
public async Task<SnAccountAuthFactor?> CreateAuthFactor(SnAccount account,
|
||||||
|
Shared.Models.AccountAuthFactorType type, string? secret)
|
||||||
{
|
{
|
||||||
SnAccountAuthFactor? factor = null;
|
SnAccountAuthFactor? factor = null;
|
||||||
switch (type)
|
switch (type)
|
||||||
@@ -352,7 +363,8 @@ public class AccountService(
|
|||||||
public async Task<SnAccountAuthFactor> EnableAuthFactor(SnAccountAuthFactor factor, string? code)
|
public async Task<SnAccountAuthFactor> EnableAuthFactor(SnAccountAuthFactor factor, string? code)
|
||||||
{
|
{
|
||||||
if (factor.EnabledAt is not null) throw new ArgumentException("The factor has been enabled.");
|
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))
|
if (code is null || !factor.VerifyPassword(code))
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
@@ -577,7 +589,8 @@ public class AccountService(
|
|||||||
await cache.RemoveAsync($"{AuthService.AuthCachePrefix}{item.Id}");
|
await cache.RemoveAsync($"{AuthService.AuthCachePrefix}{item.Id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SnAccountContact> CreateContactMethod(SnAccount account, Shared.Models.AccountContactType type, string content)
|
public async Task<SnAccountContact> CreateContactMethod(SnAccount account, Shared.Models.AccountContactType type,
|
||||||
|
string content)
|
||||||
{
|
{
|
||||||
var isExists = await db.AccountContacts
|
var isExists = await db.AccountContacts
|
||||||
.Where(x => x.AccountId == account.Id && x.Type == type && x.Content == content)
|
.Where(x => x.AccountId == account.Id && x.Type == type && x.Content == content)
|
||||||
@@ -639,7 +652,8 @@ public class AccountService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SnAccountContact> SetContactMethodPublic(SnAccount account, SnAccountContact contact, bool isPublic)
|
public async Task<SnAccountContact> SetContactMethodPublic(SnAccount account, SnAccountContact contact,
|
||||||
|
bool isPublic)
|
||||||
{
|
{
|
||||||
contact.IsPublic = isPublic;
|
contact.IsPublic = isPublic;
|
||||||
db.AccountContacts.Update(contact);
|
db.AccountContacts.Update(contact);
|
||||||
|
|||||||
Reference in New Issue
Block a user