fix(developers): distinguish API errors from taken usernames in availability check
All checks were successful
Build and Deploy / deploy (push) Successful in 40s

CheckUsernameAsync returned false (taken) on any API failure, making every
username appear taken when SilverDESK was unreachable. Now returns nullable
bool so errors show a warning instead of blocking submission.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-22 18:00:46 +00:00
parent a8d827eace
commit 33b21959d8
4 changed files with 51 additions and 8 deletions

View File

@@ -107,6 +107,10 @@
{ {
<span class="username-status username-taken">&#10007; Username is already taken</span> <span class="username-status username-taken">&#10007; Username is already taken</span>
} }
else if (_usernameCheckState == UsernameCheckState.Error)
{
<span class="username-status username-error">&#9888; Could not check availability — you can still submit</span>
}
<ValidationMessage For="() => _application.DesiredUsername" /> <ValidationMessage For="() => _application.DesiredUsername" />
</div> </div>
@@ -209,7 +213,7 @@
{ {
<div class="dev-error">@_errorMessage</div> <div class="dev-error">@_errorMessage</div>
} }
<button type="submit" class="dev-btn dev-btn-primary" disabled="@_submitting"> <button type="submit" class="dev-btn dev-btn-primary" disabled="@(_submitting || _usernameCheckState == UsernameCheckState.Taken)">
@if (_submitting) @if (_submitting)
{ {
<span class="btn-spinner"></span> <span class="btn-spinner"></span>
@@ -240,7 +244,7 @@
private readonly string[] _availablePlatforms = { "Windows", "macOS", "Linux", "Android", "iOS", "Other" }; private readonly string[] _availablePlatforms = { "Windows", "macOS", "Linux", "Android", "iOS", "Other" };
private enum UsernameCheckState { None, Checking, Available, Taken } private enum UsernameCheckState { None, Checking, Available, Taken, Error }
private void SelectRole(ApplicationRole role) private void SelectRole(ApplicationRole role)
{ {
@@ -279,7 +283,12 @@
if (!token.IsCancellationRequested) if (!token.IsCancellationRequested)
{ {
_usernameCheckState = available ? UsernameCheckState.Available : UsernameCheckState.Taken; _usernameCheckState = available switch
{
true => UsernameCheckState.Available,
false => UsernameCheckState.Taken,
null => UsernameCheckState.Error
};
StateHasChanged(); StateHasChanged();
} }
} }

View File

@@ -12,7 +12,9 @@ public static class DeveloperEndpoints
group.MapGet("/check-username/{username}", async (string username, DeveloperApplicationService service) => group.MapGet("/check-username/{username}", async (string username, DeveloperApplicationService service) =>
{ {
var available = await service.CheckUsernameAsync(username); var available = await service.CheckUsernameAsync(username);
return Results.Ok(new { available }); if (available is null)
return Results.Problem("Unable to verify username availability", statusCode: 503);
return Results.Ok(new { available = available.Value });
}); });
group.MapPost("/apply", async (DeveloperApplication application, DeveloperApplicationService service) => group.MapPost("/apply", async (DeveloperApplication application, DeveloperApplicationService service) =>

View File

@@ -17,21 +17,30 @@ public class DeveloperApplicationService
_logger = logger; _logger = logger;
} }
public async Task<bool> CheckUsernameAsync(string username) /// <summary>
/// Checks username availability. Returns: true = available, false = taken, null = error/unknown.
/// </summary>
public async Task<bool?> CheckUsernameAsync(string username)
{ {
try try
{ {
var response = await _httpClient.GetAsync($"/api/auth/check-username/{Uri.EscapeDataString(username)}"); var response = await _httpClient.GetAsync($"/api/auth/check-username/{Uri.EscapeDataString(username)}");
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
return false; {
_logger.LogWarning("Username check returned {StatusCode} for {Username}", response.StatusCode, username);
return null;
}
var result = await response.Content.ReadFromJsonAsync<JsonElement>(); var result = await response.Content.ReadFromJsonAsync<JsonElement>();
return result.TryGetProperty("available", out var available) && available.GetBoolean(); if (result.TryGetProperty("available", out var available))
return available.GetBoolean();
return null;
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error checking username availability for {Username}", username); _logger.LogError(ex, "Error checking username availability for {Username}", username);
return false; return null;
} }
} }

View File

@@ -186,6 +186,29 @@
margin-top: 0.3rem; margin-top: 0.3rem;
} }
/* Username status */
.username-status {
display: block;
font-size: 0.82rem;
margin-top: 0.35rem;
}
.username-checking {
color: rgba(255, 255, 255, 0.5);
}
.username-available {
color: #34d399;
}
.username-taken {
color: #f87171;
}
.username-error {
color: #fbbf24;
}
/* Validation messages */ /* Validation messages */
.validation-message { .validation-message {
color: #f87171; color: #f87171;