fix(developers): check username on blur instead of keystroke to avoid rate limiting
All checks were successful
Build and Deploy / deploy (push) Successful in 20s
All checks were successful
Build and Deploy / deploy (push) Successful in 20s
SilverDESK rate-limits /api/auth/check-username after ~2 requests with a 5-minute cooldown. The old 500ms debounce per keystroke quickly exhausted this limit, breaking the form. Now checks only on field blur, validates format client-side while typing, and caches results to skip redundant calls. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -92,10 +92,14 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label for="username">Desired Username</label>
|
||||
<InputText id="username" @bind-Value="_application.DesiredUsername" class="form-input" placeholder="janedoe"
|
||||
@oninput="OnUsernameInput" />
|
||||
<span class="form-hint">This will be your handle across SilverLabs services</span>
|
||||
@if (_usernameCheckState == UsernameCheckState.Checking)
|
||||
<input id="username" class="form-input" placeholder="janedoe" value="@_application.DesiredUsername"
|
||||
@oninput="OnUsernameInput" @onfocusout="OnUsernameBlur" autocomplete="off" />
|
||||
<span class="form-hint">3–30 characters: letters, numbers, hyphens and underscores</span>
|
||||
@if (!string.IsNullOrEmpty(_usernameFormatError))
|
||||
{
|
||||
<span class="username-status username-format-error">@_usernameFormatError</span>
|
||||
}
|
||||
else if (_usernameCheckState == UsernameCheckState.Checking)
|
||||
{
|
||||
<span class="username-status username-checking">Checking availability...</span>
|
||||
}
|
||||
@@ -213,7 +217,7 @@
|
||||
{
|
||||
<div class="dev-error">@_errorMessage</div>
|
||||
}
|
||||
<button type="submit" class="dev-btn dev-btn-primary" disabled="@(_submitting || _usernameCheckState == UsernameCheckState.Taken)">
|
||||
<button type="submit" class="dev-btn dev-btn-primary" disabled="@IsSubmitDisabled">
|
||||
@if (_submitting)
|
||||
{
|
||||
<span class="btn-spinner"></span>
|
||||
@@ -240,12 +244,19 @@
|
||||
private string? _errorMessage;
|
||||
|
||||
private UsernameCheckState _usernameCheckState = UsernameCheckState.None;
|
||||
private CancellationTokenSource? _usernameCheckCts;
|
||||
private string? _usernameFormatError;
|
||||
private string? _lastCheckedUsername;
|
||||
|
||||
private readonly string[] _availablePlatforms = { "Windows", "macOS", "Linux", "Android", "iOS", "Other" };
|
||||
|
||||
private static readonly System.Text.RegularExpressions.Regex UsernamePattern =
|
||||
new(@"^[a-zA-Z0-9_-]{3,30}$", System.Text.RegularExpressions.RegexOptions.Compiled);
|
||||
|
||||
private enum UsernameCheckState { None, Checking, Available, Taken, Error }
|
||||
|
||||
private bool IsSubmitDisabled =>
|
||||
_submitting || _usernameCheckState == UsernameCheckState.Taken || _usernameCheckState == UsernameCheckState.Checking;
|
||||
|
||||
private void SelectRole(ApplicationRole role)
|
||||
{
|
||||
_application.Role = role;
|
||||
@@ -259,44 +270,56 @@
|
||||
_application.Platforms.Add(platform);
|
||||
}
|
||||
|
||||
private async Task OnUsernameInput(ChangeEventArgs e)
|
||||
private void OnUsernameInput(ChangeEventArgs e)
|
||||
{
|
||||
var username = e.Value?.ToString() ?? "";
|
||||
_application.DesiredUsername = username;
|
||||
|
||||
_usernameCheckCts?.Cancel();
|
||||
|
||||
if (username.Length < 3)
|
||||
{
|
||||
// Reset API check state while typing — we'll check on blur
|
||||
_usernameCheckState = UsernameCheckState.None;
|
||||
return;
|
||||
_usernameFormatError = null;
|
||||
|
||||
// Show inline format feedback as they type
|
||||
if (username.Length > 0 && username.Length < 3)
|
||||
{
|
||||
_usernameFormatError = "Username must be at least 3 characters";
|
||||
}
|
||||
else if (username.Length > 30)
|
||||
{
|
||||
_usernameFormatError = "Username must be 30 characters or fewer";
|
||||
}
|
||||
else if (username.Length >= 3 && !UsernamePattern.IsMatch(username))
|
||||
{
|
||||
_usernameFormatError = "Only letters, numbers, hyphens and underscores allowed";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnUsernameBlur()
|
||||
{
|
||||
var username = _application.DesiredUsername?.Trim() ?? "";
|
||||
|
||||
// Don't check if empty, invalid format, or already checked this exact username
|
||||
if (string.IsNullOrEmpty(username) || !UsernamePattern.IsMatch(username))
|
||||
return;
|
||||
|
||||
if (username == _lastCheckedUsername && _usernameCheckState != UsernameCheckState.Error)
|
||||
return;
|
||||
|
||||
_usernameCheckState = UsernameCheckState.Checking;
|
||||
_usernameCheckCts = new CancellationTokenSource();
|
||||
var token = _usernameCheckCts.Token;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(500, token);
|
||||
var available = await ApplicationService.CheckUsernameAsync(username);
|
||||
_lastCheckedUsername = username;
|
||||
|
||||
if (!token.IsCancellationRequested)
|
||||
{
|
||||
_usernameCheckState = available switch
|
||||
{
|
||||
true => UsernameCheckState.Available,
|
||||
false => UsernameCheckState.Taken,
|
||||
null => UsernameCheckState.Error
|
||||
};
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// Debounce cancelled — newer keystroke took over
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleSubmit()
|
||||
{
|
||||
|
||||
@@ -209,6 +209,10 @@
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.username-format-error {
|
||||
color: #fb923c;
|
||||
}
|
||||
|
||||
/* Validation messages */
|
||||
.validation-message {
|
||||
color: #f87171;
|
||||
|
||||
Reference in New Issue
Block a user