using System.Collections.Concurrent; using System.Net.Http.Json; using System.Security.Cryptography; using System.Text; using System.Text.Json; namespace SilverLabs.Website.Services; public record PendingDeployment( string Token, string Username, string Email, string FullName, string TicketId, string? Role, DateTime CreatedAt, DateTime ExpiresAt); public class ProvisioningService { private readonly IHttpClientFactory _httpClientFactory; private readonly ILogger _logger; private readonly IConfiguration _configuration; private static readonly ConcurrentDictionary _pendingDeployments = new(); public ProvisioningService( IHttpClientFactory httpClientFactory, ILogger logger, IConfiguration configuration) { _httpClientFactory = httpClientFactory; _logger = logger; _configuration = configuration; } // --- Token management --- public PendingDeployment CreatePendingDeployment(string username, string email, string fullName, string ticketId, string? role = null) { CleanupExpiredTokens(); var token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(32)) .Replace("+", "-").Replace("/", "_").TrimEnd('='); var deployment = new PendingDeployment( token, username, email, fullName, ticketId, role, DateTime.UtcNow, DateTime.UtcNow.AddHours(48)); _pendingDeployments[token] = deployment; _logger.LogInformation("Created pending deployment for {Username} (ticket {TicketId}), token expires {ExpiresAt}", username, ticketId, deployment.ExpiresAt); return deployment; } public PendingDeployment? GetPendingDeployment(string token) { CleanupExpiredTokens(); if (_pendingDeployments.TryGetValue(token, out var deployment)) { if (deployment.ExpiresAt > DateTime.UtcNow) return deployment; _pendingDeployments.TryRemove(token, out _); } return null; } public void RemovePendingDeployment(string token) { _pendingDeployments.TryRemove(token, out _); } private void CleanupExpiredTokens() { var expired = _pendingDeployments .Where(kvp => kvp.Value.ExpiresAt <= DateTime.UtcNow) .Select(kvp => kvp.Key) .ToList(); foreach (var key in expired) _pendingDeployments.TryRemove(key, out _); } // --- Authentication --- public async Task ValidateSilverDeskCredentialsAsync(string username, string password) { try { var client = _httpClientFactory.CreateClient("SilverDesk"); var payload = new { username, password }; var response = await client.PostAsJsonAsync("/api/auth/login", payload); if (response.IsSuccessStatusCode) { _logger.LogInformation("SilverDESK credential validation succeeded for {Username}", username); return true; } _logger.LogWarning("SilverDESK credential validation failed for {Username}: {Status}", username, response.StatusCode); return false; } catch (Exception ex) { _logger.LogError(ex, "Error validating SilverDESK credentials for {Username}", username); return false; } } // --- Full provisioning with password --- public async Task<(bool Success, string Message)> ProvisionWithPasswordAsync( string ticketId, string username, string email, string fullName, string password, string? role = null) { var results = new List(); var allSuccess = true; // 1. Create Mattermost user var (mmOk, mmMsg) = await CreateMattermostUserAsync(username, email, fullName, password); results.Add($"Mattermost: {mmMsg}"); if (!mmOk) allSuccess = false; // 1b. Add to SilverLABS team (only if user was created) if (mmOk) { var (teamOk, teamMsg) = await AddMattermostUserToTeamAsync(username); results.Add($"Mattermost Team: {teamMsg}"); if (!teamOk) allSuccess = false; } // 2. Create Mailcow mailbox var (mailOk, mailMsg) = await CreateMailcowMailboxAsync(username, fullName, password); results.Add($"Mailcow: {mailMsg}"); if (!mailOk) allSuccess = false; // 3. Create Gitea user (Developers only) var giteaOk = false; if (string.Equals(role, "Developer", StringComparison.OrdinalIgnoreCase)) { var (gOk, giteaMsg) = await CreateGiteaUserAsync(username, email, fullName, password); giteaOk = gOk; results.Add($"Gitea: {giteaMsg}"); if (!giteaOk) allSuccess = false; } else { results.Add("Gitea: Skipped (not required for Tester role)"); } // 4. Update the DeveloperApplication record in SilverDESK var (updateOk, updateMsg) = await UpdateApplicationStatusAsync(ticketId, mmOk, mailOk, giteaOk); results.Add($"Application record: {updateMsg}"); if (!updateOk) allSuccess = false; var summary = string.Join("; ", results); _logger.LogInformation("Provisioning for {Username} (ticket {TicketId}): {Summary}", username, ticketId, summary); return (allSuccess, summary); } // --- Ticket replies --- public async Task<(bool Success, string Message)> SendTicketReplyAsync(string ticketId, string content, string action = "waitingcustomer") { try { var client = _httpClientFactory.CreateClient("SilverDesk"); var payload = new { content, action }; var response = await client.PostAsJsonAsync($"/api/tickets/{ticketId}/reply", payload); if (response.IsSuccessStatusCode) { _logger.LogInformation("Sent ticket reply to {TicketId} with action {Action}", ticketId, action); return (true, "Reply sent"); } var body = await response.Content.ReadAsStringAsync(); _logger.LogWarning("Failed to send ticket reply to {TicketId}: {Status} {Body}", ticketId, response.StatusCode, body); return (false, $"Failed ({response.StatusCode})"); } catch (Exception ex) { _logger.LogError(ex, "Error sending ticket reply to {TicketId}", ticketId); return (false, $"Error: {ex.Message}"); } } // --- Password sync --- public async Task<(bool Success, string Message)> SyncPasswordAsync(string username, string newPassword) { var results = new List(); var allSuccess = true; // 1. Mattermost - need to look up user ID first var (mmOk, mmMsg) = await UpdateMattermostPasswordAsync(username, newPassword); results.Add($"Mattermost: {mmMsg}"); if (!mmOk) allSuccess = false; // 2. Mailcow var (mailOk, mailMsg) = await UpdateMailcowPasswordAsync(username, newPassword); results.Add($"Mailcow: {mailMsg}"); if (!mailOk) allSuccess = false; // 3. Gitea var (giteaOk, giteaMsg) = await UpdateGiteaPasswordAsync(username, newPassword); results.Add($"Gitea: {giteaMsg}"); if (!giteaOk) allSuccess = false; var summary = string.Join("; ", results); _logger.LogInformation("Password sync for {Username}: {Summary}", username, summary); return (allSuccess, summary); } // --- Application status update --- private async Task<(bool Success, string Message)> UpdateApplicationStatusAsync( string ticketId, bool mattermostProvisioned, bool mailcowProvisioned, bool giteaProvisioned = false) { try { var client = _httpClientFactory.CreateClient("SilverDesk"); var lookupResponse = await client.GetAsync($"/api/developer-program/applications?ticketId={ticketId}"); if (!lookupResponse.IsSuccessStatusCode) { var body = await lookupResponse.Content.ReadAsStringAsync(); _logger.LogWarning("Failed to look up application by ticket {TicketId}: {Status} {Body}", ticketId, lookupResponse.StatusCode, body); return (false, $"Lookup failed ({lookupResponse.StatusCode})"); } var apps = await lookupResponse.Content.ReadFromJsonAsync(); if (apps.GetArrayLength() == 0) { _logger.LogWarning("No application found for ticket {TicketId}", ticketId); return (false, "No application found for ticket"); } var appId = apps[0].GetProperty("id").GetString(); var updatePayload = new { status = 1, // ApplicationStatus.Approved mattermostProvisioned, mailcowProvisioned, giteaProvisioned }; var updateResponse = await client.PutAsJsonAsync($"/api/developer-program/applications/{appId}", updatePayload); if (updateResponse.IsSuccessStatusCode) { _logger.LogInformation("Application {AppId} updated to Approved for ticket {TicketId}", appId, ticketId); return (true, "Updated to Approved"); } var updateBody = await updateResponse.Content.ReadAsStringAsync(); _logger.LogWarning("Failed to update application {AppId}: {Status} {Body}", appId, updateResponse.StatusCode, updateBody); return (false, $"Update failed ({updateResponse.StatusCode})"); } catch (Exception ex) { _logger.LogError(ex, "Error updating application status for ticket {TicketId}", ticketId); return (false, $"Error: {ex.Message}"); } } // --- Service account creation --- 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, fullName, password = Guid.NewGuid().ToString("N")[..16] + "!A1" }; 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}"); } } private async Task<(bool Success, string Message)> CreateMattermostUserAsync( string username, string email, string fullName, string password) { 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 }; 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}"); } } private async Task<(bool Success, string Message)> AddMattermostUserToTeamAsync(string username) { try { var client = _httpClientFactory.CreateClient("Mattermost"); // Look up user ID by username var userResponse = await client.GetAsync($"/api/v4/users/username/{username}"); if (!userResponse.IsSuccessStatusCode) return (false, $"User lookup failed ({userResponse.StatusCode})"); var userData = await userResponse.Content.ReadFromJsonAsync(); var userId = userData.GetProperty("id").GetString(); // Add to SilverLABS team var teamId = _configuration["Mattermost:TeamId"] ?? "ear83bc7nprzpe878ey7hxza7h"; var payload = new { team_id = teamId, user_id = userId }; var response = await client.PostAsJsonAsync($"/api/v4/teams/{teamId}/members", payload); if (response.IsSuccessStatusCode) return (true, "Added to team"); var body = await response.Content.ReadAsStringAsync(); _logger.LogError("Mattermost team join failed: {Status} {Body}", response.StatusCode, body); return (false, $"Team join failed ({response.StatusCode})"); } catch (Exception ex) { _logger.LogError(ex, "Mattermost team join error for {Username}", username); return (false, $"Error: {ex.Message}"); } } private async Task<(bool Success, string Message)> CreateMailcowMailboxAsync( string username, string fullName, string password) { try { var client = _httpClientFactory.CreateClient("Mailcow"); var payload = new { local_part = username, domain = "silverlabs.uk", name = fullName, password, password2 = password, quota = 1024, // 1GB active = 1, force_pw_update = 0 }; 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<(bool Success, string Message)> CreateGiteaUserAsync( string username, string email, string fullName, string password) { try { var client = _httpClientFactory.CreateClient("Gitea"); var payload = new { email, full_name = fullName, login_name = username, must_change_password = false, password, send_notify = false, username, visibility = "public" }; var json = JsonSerializer.Serialize(payload); var content = new StringContent(json, Encoding.UTF8, "application/json"); var response = await client.PostAsync("/api/v1/admin/users", content); if (response.IsSuccessStatusCode) return (true, "User created"); var body = await response.Content.ReadAsStringAsync(); _logger.LogError("Gitea user creation failed: {Status} {Body}", response.StatusCode, body); return (false, $"Failed ({response.StatusCode})"); } catch (Exception ex) { _logger.LogError(ex, "Gitea user creation error for {Username}", username); return (false, $"Error: {ex.Message}"); } } // --- Password update methods --- private async Task<(bool Success, string Message)> UpdateMattermostPasswordAsync(string username, string newPassword) { try { var client = _httpClientFactory.CreateClient("Mattermost"); // Look up user ID by username var userResponse = await client.GetAsync($"/api/v4/users/username/{username}"); if (!userResponse.IsSuccessStatusCode) { var lookupBody = await userResponse.Content.ReadAsStringAsync(); _logger.LogWarning("Mattermost user lookup failed for {Username}: {Status} {Body}", username, userResponse.StatusCode, lookupBody); return (false, $"User not found ({userResponse.StatusCode})"); } var userData = await userResponse.Content.ReadFromJsonAsync(); var userId = userData.GetProperty("id").GetString(); // Update password (admin reset — no old password needed with bot token) var payload = new { new_password = newPassword }; var response = await client.PutAsJsonAsync($"/api/v4/users/{userId}/password", payload); if (response.IsSuccessStatusCode) return (true, "Password updated"); var body = await response.Content.ReadAsStringAsync(); _logger.LogWarning("Mattermost password update failed for {Username}: {Status} {Body}", username, response.StatusCode, body); return (false, $"Failed ({response.StatusCode})"); } catch (Exception ex) { _logger.LogError(ex, "Mattermost password update error for {Username}", username); return (false, $"Error: {ex.Message}"); } } private async Task<(bool Success, string Message)> UpdateMailcowPasswordAsync(string username, string newPassword) { try { var client = _httpClientFactory.CreateClient("Mailcow"); var payload = new { items = new[] { $"{username}@silverlabs.uk" }, attr = new { password = newPassword, password2 = newPassword } }; var json = JsonSerializer.Serialize(payload); var content = new StringContent(json, Encoding.UTF8, "application/json"); var response = await client.PostAsync("/api/v1/edit/mailbox", content); if (response.IsSuccessStatusCode) return (true, "Password updated"); var body = await response.Content.ReadAsStringAsync(); _logger.LogWarning("Mailcow password update failed for {Username}: {Status} {Body}", username, response.StatusCode, body); return (false, $"Failed ({response.StatusCode})"); } catch (Exception ex) { _logger.LogError(ex, "Mailcow password update error for {Username}", username); return (false, $"Error: {ex.Message}"); } } private async Task<(bool Success, string Message)> UpdateGiteaPasswordAsync(string username, string newPassword) { try { var client = _httpClientFactory.CreateClient("Gitea"); var payload = new { login_name = username, password = newPassword, must_change_password = false, source_id = 0 }; var json = JsonSerializer.Serialize(payload); var content = new StringContent(json, Encoding.UTF8, "application/json"); var request = new HttpRequestMessage(HttpMethod.Patch, $"/api/v1/admin/users/{username}") { Content = content }; var response = await client.SendAsync(request); if (response.IsSuccessStatusCode) return (true, "Password updated"); var body = await response.Content.ReadAsStringAsync(); _logger.LogWarning("Gitea password update failed for {Username}: {Status} {Body}", username, response.StatusCode, body); return (false, $"Failed ({response.StatusCode})"); } catch (Exception ex) { _logger.LogError(ex, "Gitea password update error for {Username}", username); return (false, $"Error: {ex.Message}"); } } }