feat(developers): add developer program signup with SilverDESK integration
Add developer application page with form submission that creates tickets in SilverDESK. Includes provisioning service scaffolding for Mattermost, Mailcow, and Gitea account creation. Fixes API key header casing (X-API-Key) and ticket payload to match SilverDESK's CreateTicketDto contract. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
80
BlazorApp/Services/DeveloperApplicationService.cs
Normal file
80
BlazorApp/Services/DeveloperApplicationService.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
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;
|
||||
}
|
||||
|
||||
public async Task<(bool Success, string Message)> SubmitApplicationAsync(DeveloperApplication application)
|
||||
{
|
||||
try
|
||||
{
|
||||
var ticketBody = FormatTicketBody(application);
|
||||
|
||||
var payload = new
|
||||
{
|
||||
Subject = $"[Developer Program] {application.Role} Application - {application.FullName}",
|
||||
Description = ticketBody,
|
||||
Priority = "Medium",
|
||||
Category = "Developer Program"
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(payload);
|
||||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
|
||||
var response = await _httpClient.PostAsync("/api/tickets", content);
|
||||
|
||||
if (response.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 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.");
|
||||
}
|
||||
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.");
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatTicketBody(DeveloperApplication app)
|
||||
{
|
||||
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:** {app.Email}");
|
||||
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.Developer && !string.IsNullOrWhiteSpace(app.Skills))
|
||||
{
|
||||
sb.AppendLine("**Skills & Experience:**");
|
||||
sb.AppendLine(app.Skills);
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("**Motivation:**");
|
||||
sb.AppendLine(app.Motivation);
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
156
BlazorApp/Services/ProvisioningService.cs
Normal file
156
BlazorApp/Services/ProvisioningService.cs
Normal file
@@ -0,0 +1,156 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SilverLabs.Website.Services;
|
||||
|
||||
public class ProvisioningService
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly ILogger<ProvisioningService> _logger;
|
||||
|
||||
public ProvisioningService(IHttpClientFactory httpClientFactory, ILogger<ProvisioningService> logger)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<(bool Success, string Message)> ApproveApplicationAsync(
|
||||
int ticketId, string username, string email, string fullName)
|
||||
{
|
||||
var results = new List<string>();
|
||||
var allSuccess = true;
|
||||
|
||||
// 1. Create SilverDESK user
|
||||
var (deskOk, deskMsg) = await CreateSilverDeskUserAsync(username, email, fullName);
|
||||
results.Add($"SilverDESK: {deskMsg}");
|
||||
if (!deskOk) allSuccess = false;
|
||||
|
||||
// 2. Create Mattermost user
|
||||
var (mmOk, mmMsg) = await CreateMattermostUserAsync(username, email, fullName);
|
||||
results.Add($"Mattermost: {mmMsg}");
|
||||
if (!mmOk) allSuccess = false;
|
||||
|
||||
// 3. Create Mailcow mailbox
|
||||
var (mailOk, mailMsg) = await CreateMailcowMailboxAsync(username, fullName);
|
||||
results.Add($"Mailcow: {mailMsg}");
|
||||
if (!mailOk) allSuccess = false;
|
||||
|
||||
// 4. Update SilverDESK ticket
|
||||
if (allSuccess)
|
||||
{
|
||||
await UpdateTicketStatusAsync(ticketId, "approved", string.Join("\n", results));
|
||||
}
|
||||
|
||||
var summary = string.Join("; ", results);
|
||||
_logger.LogInformation("Provisioning for {Username}: {Summary}", username, summary);
|
||||
|
||||
return (allSuccess, summary);
|
||||
}
|
||||
|
||||
public async Task<(bool Success, string Message)> CreateSilverDeskUserAsync(string username, string email, string fullName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient("SilverDesk");
|
||||
var payload = new { username, email, name = fullName, role = "user" };
|
||||
var json = JsonSerializer.Serialize(payload);
|
||||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
|
||||
var response = await client.PostAsync("/api/users", content);
|
||||
if (response.IsSuccessStatusCode)
|
||||
return (true, "User created");
|
||||
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
_logger.LogError("SilverDESK user creation failed: {Status} {Body}", response.StatusCode, body);
|
||||
return (false, $"Failed ({response.StatusCode})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "SilverDESK user creation error for {Username}", username);
|
||||
return (false, $"Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool Success, string Message)> CreateMattermostUserAsync(string username, string email, string fullName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient("Mattermost");
|
||||
var nameParts = fullName.Split(' ', 2);
|
||||
var payload = new
|
||||
{
|
||||
email,
|
||||
username,
|
||||
first_name = nameParts[0],
|
||||
last_name = nameParts.Length > 1 ? nameParts[1] : "",
|
||||
password = Guid.NewGuid().ToString("N")[..16] + "!A1" // Temporary password
|
||||
};
|
||||
var json = JsonSerializer.Serialize(payload);
|
||||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
|
||||
var response = await client.PostAsync("/api/v4/users", content);
|
||||
if (response.IsSuccessStatusCode)
|
||||
return (true, "User created");
|
||||
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
_logger.LogError("Mattermost user creation failed: {Status} {Body}", response.StatusCode, body);
|
||||
return (false, $"Failed ({response.StatusCode})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Mattermost user creation error for {Username}", username);
|
||||
return (false, $"Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool Success, string Message)> CreateMailcowMailboxAsync(string username, string fullName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient("Mailcow");
|
||||
var payload = new
|
||||
{
|
||||
local_part = username,
|
||||
domain = "silverlabs.uk",
|
||||
name = fullName,
|
||||
password = Guid.NewGuid().ToString("N")[..16] + "!A1", // Temporary password
|
||||
password2 = "",
|
||||
quota = 1024, // 1GB
|
||||
active = 1,
|
||||
force_pw_update = 1
|
||||
};
|
||||
var json = JsonSerializer.Serialize(payload);
|
||||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
|
||||
var response = await client.PostAsync("/api/v1/add/mailbox", content);
|
||||
if (response.IsSuccessStatusCode)
|
||||
return (true, "Mailbox created");
|
||||
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
_logger.LogError("Mailcow mailbox creation failed: {Status} {Body}", response.StatusCode, body);
|
||||
return (false, $"Failed ({response.StatusCode})");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Mailcow mailbox creation error for {Username}", username);
|
||||
return (false, $"Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateTicketStatusAsync(int ticketId, string status, string note)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient("SilverDesk");
|
||||
var payload = new { status, note = $"Application approved. Provisioning results:\n{note}" };
|
||||
var json = JsonSerializer.Serialize(payload);
|
||||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||
|
||||
await client.PutAsync($"/api/tickets/{ticketId}", content);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to update ticket {TicketId} status", ticketId);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user