From cd2994d7ebb83ba4ea33331b68c981d0546c6f86 Mon Sep 17 00:00:00 2001 From: SysAdmin Date: Mon, 23 Feb 2026 15:10:45 +0000 Subject: [PATCH] fix(developers): add Mattermost team membership and role-aware Gitea provisioning New users are now added to the SilverLABS Mattermost team after account creation. Gitea provisioning is skipped for Testers (only Developers get repo access). Role is parsed from ticket description and threaded through the entire approval/confirmation flow. Gitea API token is now configured. Co-Authored-By: Claude Opus 4.6 --- BlazorApp/Endpoints/DeveloperEndpoints.cs | 28 +++++--- .../Services/DeveloperTicketParsingService.cs | 5 +- BlazorApp/Services/ProvisioningService.cs | 65 +++++++++++++++++-- BlazorApp/appsettings.json | 5 +- 4 files changed, 82 insertions(+), 21 deletions(-) diff --git a/BlazorApp/Endpoints/DeveloperEndpoints.cs b/BlazorApp/Endpoints/DeveloperEndpoints.cs index 5e7df84..888b28b 100644 --- a/BlazorApp/Endpoints/DeveloperEndpoints.cs +++ b/BlazorApp/Endpoints/DeveloperEndpoints.cs @@ -43,18 +43,22 @@ public static class DeveloperEndpoints return Results.Problem("Failed to fetch ticket from SilverDESK", statusCode: 502); var description = ticket.Value.GetProperty("description").GetString() ?? ""; - var (fullName, email, desiredUsername) = ticketService.ParseApplicationFromDescription(description); + var (fullName, email, desiredUsername, role) = ticketService.ParseApplicationFromDescription(description); if (string.IsNullOrEmpty(fullName) || string.IsNullOrEmpty(email) || string.IsNullOrEmpty(desiredUsername)) return Results.Problem("Could not parse applicant details from ticket description", statusCode: 422); // Generate confirmation token instead of provisioning immediately - var deployment = provisioningService.CreatePendingDeployment(desiredUsername, email, fullName, ticketId); + var deployment = provisioningService.CreatePendingDeployment(desiredUsername, email, fullName, ticketId, role); var siteBase = config["SiteBaseUrl"] ?? "https://silverlabs.uk"; var confirmUrl = $"{siteBase}/developers/confirm/{deployment.Token}"; // Send ticket reply with confirmation link + var giteaLine = string.Equals(role, "Developer", StringComparison.OrdinalIgnoreCase) + ? "\n- **Gitea**: Source code repository access" + : ""; + var replyContent = $""" Your application has been approved! To activate your accounts, please confirm your identity: @@ -64,8 +68,7 @@ public static class DeveloperEndpoints Once confirmed, the following accounts will be created for you: - **Email**: {desiredUsername}@silverlabs.uk - - **Mattermost**: Team chat access - - **Gitea**: Source code repository access + - **Mattermost**: Team chat access{giteaLine} """; var (replyOk, replyMsg) = await provisioningService.SendTicketReplyAsync(ticketId, replyContent); @@ -112,7 +115,15 @@ public static class DeveloperEndpoints // Provision all services with the user's password var (success, message) = await provisioningService.ProvisionWithPasswordAsync( - deployment.TicketId, deployment.Username, deployment.Email, deployment.FullName, request.Password); + deployment.TicketId, deployment.Username, deployment.Email, deployment.FullName, request.Password, deployment.Role); + + var isDeveloper = string.Equals(deployment.Role, "Developer", StringComparison.OrdinalIgnoreCase); + var giteaSuccessSection = isDeveloper + ? $"\n\n**Gitea** (Source Code): [git.silverlabs.uk](https://git.silverlabs.uk)" + : ""; + var giteaFailSection = isDeveloper + ? $"\n- **Gitea**: [git.silverlabs.uk](https://git.silverlabs.uk)" + : ""; // Send follow-up ticket reply with results var resultContent = success @@ -124,9 +135,7 @@ public static class DeveloperEndpoints - IMAP: `mail.silverlined.uk:993` (SSL) - SMTP: `mail.silverlined.uk:465` (SSL) - **Mattermost** (Team Chat): [ops.silverlined.uk](https://ops.silverlined.uk) - - **Gitea** (Source Code): [git.silverlabs.uk](https://git.silverlabs.uk) + **Mattermost** (Team Chat): [ops.silverlined.uk](https://ops.silverlined.uk){giteaSuccessSection} **SilverDESK** (Support & Tickets): [silverdesk.silverlabs.uk](https://silverdesk.silverlabs.uk) @@ -144,8 +153,7 @@ public static class DeveloperEndpoints Once resolved, your services will be: - **Email**: {deployment.Username}@silverlabs.uk — [mail.silverlined.uk](https://mail.silverlined.uk) - - **Mattermost**: [ops.silverlined.uk](https://ops.silverlined.uk) - - **Gitea**: [git.silverlabs.uk](https://git.silverlabs.uk) + - **Mattermost**: [ops.silverlined.uk](https://ops.silverlined.uk){giteaFailSection} """; await provisioningService.SendTicketReplyAsync(deployment.TicketId, resultContent, "close"); diff --git a/BlazorApp/Services/DeveloperTicketParsingService.cs b/BlazorApp/Services/DeveloperTicketParsingService.cs index d819cb2..e0dece1 100644 --- a/BlazorApp/Services/DeveloperTicketParsingService.cs +++ b/BlazorApp/Services/DeveloperTicketParsingService.cs @@ -35,13 +35,14 @@ public class DeveloperTicketParsingService } } - public (string? FullName, string? Email, string? DesiredUsername) ParseApplicationFromDescription(string description) + public (string? FullName, string? Email, string? DesiredUsername, string? Role) ParseApplicationFromDescription(string description) { var fullName = ExtractField(description, @"\*\*Full Name:\*\*\s*(.+)"); var email = ExtractField(description, @"\*\*Email:\*\*\s*(.+)"); var desiredUsername = ExtractField(description, @"\*\*Desired Username:\*\*\s*(.+)"); + var role = ExtractField(description, @"\*\*Role:\*\*\s*(.+)"); - return (fullName, email, desiredUsername); + return (fullName, email, desiredUsername, role); } private static string? ExtractField(string text, string pattern) diff --git a/BlazorApp/Services/ProvisioningService.cs b/BlazorApp/Services/ProvisioningService.cs index 9d6ba8c..33847be 100644 --- a/BlazorApp/Services/ProvisioningService.cs +++ b/BlazorApp/Services/ProvisioningService.cs @@ -12,6 +12,7 @@ public record PendingDeployment( string Email, string FullName, string TicketId, + string? Role, DateTime CreatedAt, DateTime ExpiresAt); @@ -35,7 +36,7 @@ public class ProvisioningService // --- Token management --- - public PendingDeployment CreatePendingDeployment(string username, string email, string fullName, string ticketId) + public PendingDeployment CreatePendingDeployment(string username, string email, string fullName, string ticketId, string? role = null) { CleanupExpiredTokens(); @@ -43,7 +44,7 @@ public class ProvisioningService .Replace("+", "-").Replace("/", "_").TrimEnd('='); var deployment = new PendingDeployment( - token, username, email, fullName, ticketId, + token, username, email, fullName, ticketId, role, DateTime.UtcNow, DateTime.UtcNow.AddHours(48)); _pendingDeployments[token] = deployment; @@ -113,7 +114,7 @@ public class ProvisioningService // --- Full provisioning with password --- public async Task<(bool Success, string Message)> ProvisionWithPasswordAsync( - string ticketId, string username, string email, string fullName, string password) + string ticketId, string username, string email, string fullName, string password, string? role = null) { var results = new List(); var allSuccess = true; @@ -123,15 +124,32 @@ public class ProvisioningService 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 - var (giteaOk, giteaMsg) = await CreateGiteaUserAsync(username, email, fullName, password); - results.Add($"Gitea: {giteaMsg}"); - if (!giteaOk) 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); @@ -318,6 +336,39 @@ public class ProvisioningService } } + 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) { diff --git a/BlazorApp/appsettings.json b/BlazorApp/appsettings.json index bce772f..4237b43 100644 --- a/BlazorApp/appsettings.json +++ b/BlazorApp/appsettings.json @@ -12,7 +12,8 @@ }, "Mattermost": { "BaseUrl": "https://ops.silverlined.uk", - "ApiToken": "ktmfkpxz7ffr5g1imuqg8hm58c" + "ApiToken": "ktmfkpxz7ffr5g1imuqg8hm58c", + "TeamId": "ear83bc7nprzpe878ey7hxza7h" }, "Mailcow": { "BaseUrl": "https://mail.silverlined.uk", @@ -20,7 +21,7 @@ }, "Gitea": { "BaseUrl": "https://git.silverlabs.uk", - "ApiToken": "" + "ApiToken": "70ec152b27ee12d8a2cfb7241df5735351df72cd" }, "SiteBaseUrl": "https://silverlabs.uk", "AdminApiKey": "aawb2MHblbfmqdhcS7Xp2/ibQOUbUE1BDoqdJOu0bjM="