fix(developers): add Mattermost team membership and role-aware Gitea provisioning
All checks were successful
Build and Deploy / deploy (push) Successful in 18s

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 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 15:10:45 +00:00
parent dc9a60a7a2
commit cd2994d7eb
4 changed files with 82 additions and 21 deletions

View File

@@ -43,18 +43,22 @@ public static class DeveloperEndpoints
return Results.Problem("Failed to fetch ticket from SilverDESK", statusCode: 502); return Results.Problem("Failed to fetch ticket from SilverDESK", statusCode: 502);
var description = ticket.Value.GetProperty("description").GetString() ?? ""; 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)) if (string.IsNullOrEmpty(fullName) || string.IsNullOrEmpty(email) || string.IsNullOrEmpty(desiredUsername))
return Results.Problem("Could not parse applicant details from ticket description", statusCode: 422); return Results.Problem("Could not parse applicant details from ticket description", statusCode: 422);
// Generate confirmation token instead of provisioning immediately // 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 siteBase = config["SiteBaseUrl"] ?? "https://silverlabs.uk";
var confirmUrl = $"{siteBase}/developers/confirm/{deployment.Token}"; var confirmUrl = $"{siteBase}/developers/confirm/{deployment.Token}";
// Send ticket reply with confirmation link // Send ticket reply with confirmation link
var giteaLine = string.Equals(role, "Developer", StringComparison.OrdinalIgnoreCase)
? "\n- **Gitea**: Source code repository access"
: "";
var replyContent = $""" var replyContent = $"""
Your application has been approved! To activate your accounts, please confirm your identity: 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: Once confirmed, the following accounts will be created for you:
- **Email**: {desiredUsername}@silverlabs.uk - **Email**: {desiredUsername}@silverlabs.uk
- **Mattermost**: Team chat access - **Mattermost**: Team chat access{giteaLine}
- **Gitea**: Source code repository access
"""; """;
var (replyOk, replyMsg) = await provisioningService.SendTicketReplyAsync(ticketId, replyContent); var (replyOk, replyMsg) = await provisioningService.SendTicketReplyAsync(ticketId, replyContent);
@@ -112,7 +115,15 @@ public static class DeveloperEndpoints
// Provision all services with the user's password // Provision all services with the user's password
var (success, message) = await provisioningService.ProvisionWithPasswordAsync( 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 // Send follow-up ticket reply with results
var resultContent = success var resultContent = success
@@ -124,9 +135,7 @@ public static class DeveloperEndpoints
- IMAP: `mail.silverlined.uk:993` (SSL) - IMAP: `mail.silverlined.uk:993` (SSL)
- SMTP: `mail.silverlined.uk:465` (SSL) - SMTP: `mail.silverlined.uk:465` (SSL)
**Mattermost** (Team Chat): [ops.silverlined.uk](https://ops.silverlined.uk) **Mattermost** (Team Chat): [ops.silverlined.uk](https://ops.silverlined.uk){giteaSuccessSection}
**Gitea** (Source Code): [git.silverlabs.uk](https://git.silverlabs.uk)
**SilverDESK** (Support & Tickets): [silverdesk.silverlabs.uk](https://silverdesk.silverlabs.uk) **SilverDESK** (Support & Tickets): [silverdesk.silverlabs.uk](https://silverdesk.silverlabs.uk)
@@ -144,8 +153,7 @@ public static class DeveloperEndpoints
Once resolved, your services will be: Once resolved, your services will be:
- **Email**: {deployment.Username}@silverlabs.uk — [mail.silverlined.uk](https://mail.silverlined.uk) - **Email**: {deployment.Username}@silverlabs.uk — [mail.silverlined.uk](https://mail.silverlined.uk)
- **Mattermost**: [ops.silverlined.uk](https://ops.silverlined.uk) - **Mattermost**: [ops.silverlined.uk](https://ops.silverlined.uk){giteaFailSection}
- **Gitea**: [git.silverlabs.uk](https://git.silverlabs.uk)
"""; """;
await provisioningService.SendTicketReplyAsync(deployment.TicketId, resultContent, "close"); await provisioningService.SendTicketReplyAsync(deployment.TicketId, resultContent, "close");

View File

@@ -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 fullName = ExtractField(description, @"\*\*Full Name:\*\*\s*(.+)");
var email = ExtractField(description, @"\*\*Email:\*\*\s*(.+)"); var email = ExtractField(description, @"\*\*Email:\*\*\s*(.+)");
var desiredUsername = ExtractField(description, @"\*\*Desired Username:\*\*\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) private static string? ExtractField(string text, string pattern)

View File

@@ -12,6 +12,7 @@ public record PendingDeployment(
string Email, string Email,
string FullName, string FullName,
string TicketId, string TicketId,
string? Role,
DateTime CreatedAt, DateTime CreatedAt,
DateTime ExpiresAt); DateTime ExpiresAt);
@@ -35,7 +36,7 @@ public class ProvisioningService
// --- Token management --- // --- 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(); CleanupExpiredTokens();
@@ -43,7 +44,7 @@ public class ProvisioningService
.Replace("+", "-").Replace("/", "_").TrimEnd('='); .Replace("+", "-").Replace("/", "_").TrimEnd('=');
var deployment = new PendingDeployment( var deployment = new PendingDeployment(
token, username, email, fullName, ticketId, token, username, email, fullName, ticketId, role,
DateTime.UtcNow, DateTime.UtcNow.AddHours(48)); DateTime.UtcNow, DateTime.UtcNow.AddHours(48));
_pendingDeployments[token] = deployment; _pendingDeployments[token] = deployment;
@@ -113,7 +114,7 @@ public class ProvisioningService
// --- Full provisioning with password --- // --- Full provisioning with password ---
public async Task<(bool Success, string Message)> ProvisionWithPasswordAsync( 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<string>(); var results = new List<string>();
var allSuccess = true; var allSuccess = true;
@@ -123,15 +124,32 @@ public class ProvisioningService
results.Add($"Mattermost: {mmMsg}"); results.Add($"Mattermost: {mmMsg}");
if (!mmOk) allSuccess = false; 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 // 2. Create Mailcow mailbox
var (mailOk, mailMsg) = await CreateMailcowMailboxAsync(username, fullName, password); var (mailOk, mailMsg) = await CreateMailcowMailboxAsync(username, fullName, password);
results.Add($"Mailcow: {mailMsg}"); results.Add($"Mailcow: {mailMsg}");
if (!mailOk) allSuccess = false; if (!mailOk) allSuccess = false;
// 3. Create Gitea user // 3. Create Gitea user (Developers only)
var (giteaOk, giteaMsg) = await CreateGiteaUserAsync(username, email, fullName, password); var giteaOk = false;
results.Add($"Gitea: {giteaMsg}"); if (string.Equals(role, "Developer", StringComparison.OrdinalIgnoreCase))
if (!giteaOk) allSuccess = false; {
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 // 4. Update the DeveloperApplication record in SilverDESK
var (updateOk, updateMsg) = await UpdateApplicationStatusAsync(ticketId, mmOk, mailOk, giteaOk); 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<JsonElement>();
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( private async Task<(bool Success, string Message)> CreateMailcowMailboxAsync(
string username, string fullName, string password) string username, string fullName, string password)
{ {

View File

@@ -12,7 +12,8 @@
}, },
"Mattermost": { "Mattermost": {
"BaseUrl": "https://ops.silverlined.uk", "BaseUrl": "https://ops.silverlined.uk",
"ApiToken": "ktmfkpxz7ffr5g1imuqg8hm58c" "ApiToken": "ktmfkpxz7ffr5g1imuqg8hm58c",
"TeamId": "ear83bc7nprzpe878ey7hxza7h"
}, },
"Mailcow": { "Mailcow": {
"BaseUrl": "https://mail.silverlined.uk", "BaseUrl": "https://mail.silverlined.uk",
@@ -20,7 +21,7 @@
}, },
"Gitea": { "Gitea": {
"BaseUrl": "https://git.silverlabs.uk", "BaseUrl": "https://git.silverlabs.uk",
"ApiToken": "" "ApiToken": "70ec152b27ee12d8a2cfb7241df5735351df72cd"
}, },
"SiteBaseUrl": "https://silverlabs.uk", "SiteBaseUrl": "https://silverlabs.uk",
"AdminApiKey": "aawb2MHblbfmqdhcS7Xp2/ibQOUbUE1BDoqdJOu0bjM=" "AdminApiKey": "aawb2MHblbfmqdhcS7Xp2/ibQOUbUE1BDoqdJOu0bjM="