-
+ This will be your handle across SilverLabs services
+ @if (_usernameCheckState == UsernameCheckState.Checking)
+ {
+ Checking availability...
+ }
+ else if (_usernameCheckState == UsernameCheckState.Available)
+ {
+ ✓ Username is available
+ }
+ else if (_usernameCheckState == UsernameCheckState.Taken)
+ {
+ ✗ Username is already taken
+ }
@@ -101,6 +118,26 @@
+
+
+
Create Your Password
+
This will be your password for SilverDESK and associated services.
+
+
+
+
+ Must include uppercase, lowercase, and a number
+
+
+
+
+
+
+
+
+
+
+
Devices & Platforms
@@ -198,8 +235,13 @@
private string? _resultMessage;
private string? _errorMessage;
+ private UsernameCheckState _usernameCheckState = UsernameCheckState.None;
+ private CancellationTokenSource? _usernameCheckCts;
+
private readonly string[] _availablePlatforms = { "Windows", "macOS", "Linux", "Android", "iOS", "Other" };
+ private enum UsernameCheckState { None, Checking, Available, Taken }
+
private void SelectRole(ApplicationRole role)
{
_application.Role = role;
@@ -213,6 +255,40 @@
_application.Platforms.Add(platform);
}
+ private async Task OnUsernameInput(ChangeEventArgs e)
+ {
+ var username = e.Value?.ToString() ?? "";
+ _application.DesiredUsername = username;
+
+ _usernameCheckCts?.Cancel();
+
+ if (username.Length < 3)
+ {
+ _usernameCheckState = UsernameCheckState.None;
+ return;
+ }
+
+ _usernameCheckState = UsernameCheckState.Checking;
+ _usernameCheckCts = new CancellationTokenSource();
+ var token = _usernameCheckCts.Token;
+
+ try
+ {
+ await Task.Delay(500, token);
+ var available = await ApplicationService.CheckUsernameAsync(username);
+
+ if (!token.IsCancellationRequested)
+ {
+ _usernameCheckState = available ? UsernameCheckState.Available : UsernameCheckState.Taken;
+ StateHasChanged();
+ }
+ }
+ catch (TaskCanceledException)
+ {
+ // Debounce cancelled — newer keystroke took over
+ }
+ }
+
private async Task HandleSubmit()
{
_errorMessage = null;
@@ -220,7 +296,7 @@
try
{
- var (success, message) = await ApplicationService.SubmitApplicationAsync(_application);
+ var (success, message, token) = await ApplicationService.SubmitApplicationAsync(_application);
if (success)
{
diff --git a/BlazorApp/Endpoints/DeveloperEndpoints.cs b/BlazorApp/Endpoints/DeveloperEndpoints.cs
index 655b103..46c6fdc 100644
--- a/BlazorApp/Endpoints/DeveloperEndpoints.cs
+++ b/BlazorApp/Endpoints/DeveloperEndpoints.cs
@@ -9,11 +9,17 @@ public static class DeveloperEndpoints
{
var group = app.MapGroup("/api/developers");
+ group.MapGet("/check-username/{username}", async (string username, DeveloperApplicationService service) =>
+ {
+ var available = await service.CheckUsernameAsync(username);
+ return Results.Ok(new { available });
+ });
+
group.MapPost("/apply", async (DeveloperApplication application, DeveloperApplicationService service) =>
{
- var (success, message) = await service.SubmitApplicationAsync(application);
+ var (success, message, token) = await service.SubmitApplicationAsync(application);
return success
- ? Results.Ok(new { message })
+ ? Results.Ok(new { message, token })
: Results.Problem(message, statusCode: 502);
});
diff --git a/BlazorApp/Models/DeveloperApplication.cs b/BlazorApp/Models/DeveloperApplication.cs
index accdb29..a254fee 100644
--- a/BlazorApp/Models/DeveloperApplication.cs
+++ b/BlazorApp/Models/DeveloperApplication.cs
@@ -28,6 +28,14 @@ public class DeveloperApplication
public string? Skills { get; set; }
+ [Required(ErrorMessage = "Password is required")]
+ [StringLength(100, MinimumLength = 8, ErrorMessage = "Password must be at least 8 characters")]
+ public string Password { get; set; } = string.Empty;
+
+ [Required(ErrorMessage = "Please confirm your password")]
+ [Compare("Password", ErrorMessage = "Passwords do not match")]
+ public string ConfirmPassword { get; set; } = string.Empty;
+
[Required(ErrorMessage = "Please tell us why you want to join")]
[StringLength(2000, MinimumLength = 20, ErrorMessage = "Motivation must be between 20 and 2000 characters")]
public string Motivation { get; set; } = string.Empty;
diff --git a/BlazorApp/Services/DeveloperApplicationService.cs b/BlazorApp/Services/DeveloperApplicationService.cs
index 046ccdc..40f8ce3 100644
--- a/BlazorApp/Services/DeveloperApplicationService.cs
+++ b/BlazorApp/Services/DeveloperApplicationService.cs
@@ -1,3 +1,5 @@
+using System.Net.Http.Headers;
+using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using SilverLabs.Website.Models;
@@ -15,13 +17,54 @@ public class DeveloperApplicationService
_logger = logger;
}
- public async Task<(bool Success, string Message)> SubmitApplicationAsync(DeveloperApplication application)
+ public async Task CheckUsernameAsync(string username)
{
try
{
- var ticketBody = FormatTicketBody(application);
+ var response = await _httpClient.GetAsync($"/api/auth/check-username/{Uri.EscapeDataString(username)}");
+ if (!response.IsSuccessStatusCode)
+ return false;
- var payload = new
+ var result = await response.Content.ReadFromJsonAsync();
+ return result.TryGetProperty("available", out var available) && available.GetBoolean();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error checking username availability for {Username}", username);
+ return false;
+ }
+ }
+
+ public async Task<(bool Success, string Message, string? Token)> SubmitApplicationAsync(DeveloperApplication application)
+ {
+ try
+ {
+ // 1. Register user on SilverDESK
+ var registerPayload = new
+ {
+ username = application.DesiredUsername,
+ email = application.Email,
+ password = application.Password,
+ fullName = application.FullName
+ };
+
+ var registerResponse = await _httpClient.PostAsJsonAsync("/api/auth/register", registerPayload);
+
+ if (!registerResponse.IsSuccessStatusCode)
+ {
+ var errorBody = await registerResponse.Content.ReadAsStringAsync();
+ _logger.LogError("SilverDESK registration failed: {StatusCode} - {Body}", registerResponse.StatusCode, errorBody);
+
+ var friendlyMessage = ParseRegistrationError(errorBody);
+ return (false, friendlyMessage, null);
+ }
+
+ var authResult = await registerResponse.Content.ReadFromJsonAsync();
+ var token = authResult.GetProperty("token").GetString();
+
+ // 2. Create ticket using the user's own JWT
+ var ticketBody = FormatTicketBody(application);
+ var ticketPayload = new
{
Subject = $"[Developer Program] {application.Role} Application - {application.FullName}",
Description = ticketBody,
@@ -29,28 +72,54 @@ public class DeveloperApplicationService
Category = "Developer Program"
};
- var json = JsonSerializer.Serialize(payload);
- var content = new StringContent(json, Encoding.UTF8, "application/json");
+ var ticketRequest = new HttpRequestMessage(HttpMethod.Post, "/api/tickets");
+ ticketRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
+ ticketRequest.Content = JsonContent.Create(ticketPayload);
- var response = await _httpClient.PostAsync("/api/tickets", content);
+ var ticketResponse = await _httpClient.SendAsync(ticketRequest);
- if (response.IsSuccessStatusCode)
+ if (!ticketResponse.IsSuccessStatusCode)
{
- _logger.LogInformation("Developer application submitted for {Email} as {Role}", application.Email, application.Role);
- return (true, "Application submitted successfully! We'll review it and get back to you soon.");
+ var errorBody = await ticketResponse.Content.ReadAsStringAsync();
+ _logger.LogError("Failed to create ticket: {StatusCode} - {Body}", ticketResponse.StatusCode, errorBody);
+ // User was created but ticket failed — still return success with a note
+ return (true, "Your account has been created, but we had trouble submitting your application ticket. Please log in to SilverDESK and create a support ticket.", token);
}
- var errorBody = await response.Content.ReadAsStringAsync();
- _logger.LogError("Failed to create ticket: {StatusCode} - {Body}", response.StatusCode, errorBody);
- return (false, "Something went wrong submitting your application. Please try again later.");
+ _logger.LogInformation("Developer application submitted for {Email} as {Role} — user registered and ticket created",
+ application.Email, application.Role);
+
+ return (true, "Application submitted successfully! Your SilverDESK account has been created.", token);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error submitting developer application for {Email}", application.Email);
- return (false, "Unable to connect to the application service. Please try again later.");
+ return (false, "Unable to connect to the application service. Please try again later.", null);
}
}
+ private static string ParseRegistrationError(string errorBody)
+ {
+ try
+ {
+ var error = JsonSerializer.Deserialize(errorBody);
+ if (error.TryGetProperty("message", out var message))
+ {
+ var msg = message.GetString() ?? "";
+ if (msg.Contains("Username already exists", StringComparison.OrdinalIgnoreCase))
+ return "That username is already taken. Please choose a different one.";
+ if (msg.Contains("Email already exists", StringComparison.OrdinalIgnoreCase))
+ return "An account with that email already exists.";
+ if (msg.Contains("Password", StringComparison.OrdinalIgnoreCase))
+ return msg;
+ return msg;
+ }
+ }
+ catch { }
+
+ return "Something went wrong creating your account. Please try again later.";
+ }
+
private static string FormatTicketBody(DeveloperApplication app)
{
var sb = new StringBuilder();
diff --git a/BlazorApp/Services/ProvisioningService.cs b/BlazorApp/Services/ProvisioningService.cs
index 89df7c5..3f1c018 100644
--- a/BlazorApp/Services/ProvisioningService.cs
+++ b/BlazorApp/Services/ProvisioningService.cs
@@ -20,17 +20,15 @@ public class ProvisioningService
var results = new List();
var allSuccess = true;
- // 1. Create SilverDESK user
- var (deskOk, deskMsg) = await CreateSilverDeskUserAsync(username, email, fullName);
- results.Add($"SilverDESK: {deskMsg}");
- if (!deskOk) allSuccess = false;
+ // SilverDESK user already exists (created at application time)
+ // Only provision external services
- // 2. Create Mattermost user
+ // 1. Create Mattermost user
var (mmOk, mmMsg) = await CreateMattermostUserAsync(username, email, fullName);
results.Add($"Mattermost: {mmMsg}");
if (!mmOk) allSuccess = false;
- // 3. Create Mailcow mailbox
+ // 2. Create Mailcow mailbox
var (mailOk, mailMsg) = await CreateMailcowMailboxAsync(username, fullName);
results.Add($"Mailcow: {mailMsg}");
if (!mailOk) allSuccess = false;