Files
Website/BlazorApp/Services/DeveloperApplicationService.cs
SysAdmin 502d48da99
All checks were successful
Build and Deploy / deploy (push) Successful in 42s
app update
2026-02-24 14:48:02 +00:00

286 lines
12 KiB
C#

using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using SilverLabs.Website.Models;
namespace SilverLabs.Website.Services;
public class DeveloperApplicationService
{
private readonly HttpClient _httpClient;
private readonly ILogger<DeveloperApplicationService> _logger;
public DeveloperApplicationService(HttpClient httpClient, ILogger<DeveloperApplicationService> logger)
{
_httpClient = httpClient;
_logger = logger;
}
/// <summary>
/// Checks username availability. Returns: true = available, false = taken, null = error/unknown.
/// </summary>
public async Task<bool?> CheckUsernameAsync(string username)
{
try
{
var response = await _httpClient.GetAsync($"/api/auth/check-username/{Uri.EscapeDataString(username)}");
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning("Username check returned {StatusCode} for {Username}", response.StatusCode, username);
return null;
}
var result = await response.Content.ReadFromJsonAsync<JsonElement>();
if (result.TryGetProperty("available", out var available))
return available.GetBoolean();
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error checking username availability for {Username}", username);
return null;
}
}
public async Task<(bool Success, string Message, string? Token)> SubmitApplicationAsync(DeveloperApplication application)
{
try
{
// Use silverlabs.uk address when no personal email provided
var effectiveEmail = string.IsNullOrWhiteSpace(application.Email)
? $"{application.DesiredUsername}@silverlabs.uk"
: application.Email.Trim();
// 1. Register user on SilverDESK
var registerPayload = new
{
username = application.DesiredUsername,
email = effectiveEmail,
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<JsonElement>();
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,
Priority = "Medium",
Category = "Developer Program"
};
// Use a fresh HttpClient without the X-API-Key default header so that
// SilverDESK's MultiAuth policy routes to Bearer/JWT auth (the new user's token)
// instead of ApiKey auth (which resolves to the MCP system user).
using var userClient = new HttpClient { BaseAddress = _httpClient.BaseAddress };
var ticketRequest = new HttpRequestMessage(HttpMethod.Post, "/api/tickets");
ticketRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
ticketRequest.Content = JsonContent.Create(ticketPayload);
var ticketResponse = await userClient.SendAsync(ticketRequest);
if (!ticketResponse.IsSuccessStatusCode)
{
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);
}
// 3. Create DeveloperApplication record linking user + ticket
try
{
var userId = authResult.GetProperty("user").GetProperty("id").GetString();
var ticketResult = await ticketResponse.Content.ReadFromJsonAsync<JsonElement>();
var ticketId = ticketResult.GetProperty("id").GetString();
var applicationPayload = new
{
userId,
ticketId,
fullName = application.FullName,
email = effectiveEmail,
desiredUsername = application.DesiredUsername,
timezone = application.Timezone,
appliedRole = application.Role.ToString(),
platforms = application.Platforms,
skills = SerializeAssessment(application),
motivation = GenerateMotivationSummary(application),
status = 0, // Pending
silverDeskProvisioned = true
};
var appResponse = await _httpClient.PostAsJsonAsync("/api/developer-program/applications", applicationPayload);
if (appResponse.IsSuccessStatusCode)
{
_logger.LogInformation("DeveloperApplication record created for {Email}", effectiveEmail);
}
else
{
var appError = await appResponse.Content.ReadAsStringAsync();
_logger.LogWarning("Failed to create DeveloperApplication record for {Email}: {StatusCode} - {Body}",
effectiveEmail, appResponse.StatusCode, appError);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to create DeveloperApplication record for {Email} — user and ticket were created successfully",
effectiveEmail);
}
_logger.LogInformation("Developer application submitted for {Email} as {Role} — user registered and ticket created",
effectiveEmail, 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 {Username}", application.DesiredUsername);
return (false, "Unable to connect to the application service. Please try again later.", null);
}
}
/// <summary>
/// Serializes structured assessment data as JSON for the Skills column.
/// </summary>
internal static string SerializeAssessment(DeveloperApplication app)
{
object data;
if (app.Role == ApplicationRole.Tester)
{
data = new
{
type = "tester",
internetUnderstanding = app.InternetUnderstanding ?? 0,
enjoysTesting = app.EnjoysTesting ?? 0,
additionalNotes = app.AdditionalNotes ?? ""
};
}
else
{
data = new
{
type = "developer",
experienceRange = app.ExperienceRange ?? "",
selectedSkills = app.SelectedSkills,
additionalNotes = app.AdditionalNotes ?? ""
};
}
return JsonSerializer.Serialize(data, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
}
/// <summary>
/// Generates a human-readable summary for the Motivation field (backward compatibility).
/// </summary>
internal static string GenerateMotivationSummary(DeveloperApplication app)
{
if (app.Role == ApplicationRole.Tester)
{
var summary = $"Internet understanding: {app.InternetUnderstanding}/5, Testing enthusiasm: {app.EnjoysTesting}/5";
if (!string.IsNullOrWhiteSpace(app.AdditionalNotes))
summary += $". Notes: {app.AdditionalNotes.Trim()}";
return summary;
}
else
{
var skills = app.SelectedSkills.Count > 0
? string.Join(", ", app.SelectedSkills)
: "None selected";
var summary = $"{app.ExperienceRange} experience. Skills: {skills}";
if (!string.IsNullOrWhiteSpace(app.AdditionalNotes))
summary += $". Notes: {app.AdditionalNotes.Trim()}";
return summary;
}
}
private static string ParseRegistrationError(string errorBody)
{
try
{
var error = JsonSerializer.Deserialize<JsonElement>(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 effectiveEmail = string.IsNullOrWhiteSpace(app.Email)
? $"{app.DesiredUsername}@silverlabs.uk"
: app.Email.Trim();
var sb = new StringBuilder();
sb.AppendLine("## Developer Program Application");
sb.AppendLine();
sb.AppendLine($"**Role:** {app.Role}");
sb.AppendLine($"**Full Name:** {app.FullName}");
sb.AppendLine($"**Email:** {effectiveEmail}");
sb.AppendLine($"**Desired Username:** {app.DesiredUsername}");
sb.AppendLine($"**Timezone:** {app.Timezone}");
sb.AppendLine();
sb.AppendLine($"**Platforms:** {string.Join(", ", app.Platforms)}");
sb.AppendLine();
if (app.Role == ApplicationRole.Tester)
{
sb.AppendLine("### Assessment");
sb.AppendLine($"- Internet understanding: {"*".PadLeft(app.InternetUnderstanding ?? 0, '*')}{new string('-', 5 - (app.InternetUnderstanding ?? 0))} ({app.InternetUnderstanding}/5)");
sb.AppendLine($"- Testing enthusiasm: {"*".PadLeft(app.EnjoysTesting ?? 0, '*')}{new string('-', 5 - (app.EnjoysTesting ?? 0))} ({app.EnjoysTesting}/5)");
}
else
{
sb.AppendLine("### Skills & Experience");
sb.AppendLine($"**Experience:** {app.ExperienceRange}");
sb.AppendLine();
if (app.SelectedSkills.Count > 0)
{
sb.AppendLine($"**Technologies:** {string.Join(", ", app.SelectedSkills)}");
}
}
if (!string.IsNullOrWhiteSpace(app.AdditionalNotes))
{
sb.AppendLine();
sb.AppendLine("### Additional Notes");
sb.AppendLine(app.AdditionalNotes.Trim());
}
return sb.ToString();
}
}