286 lines
12 KiB
C#
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();
|
|
}
|
|
}
|