feat(developers): add password-synced provisioning and deployment confirmation flow
All checks were successful
Build and Deploy / deploy (push) Successful in 41s

Replace random unrecoverable passwords with a confirmation-based flow:
admin approval generates a secure token and sends a ticket reply with a
confirmation link; the developer clicks the link, enters their SilverDESK
password, and all services (Mattermost, Mailcow, Gitea) are provisioned
with that password. Adds password sync endpoint for SilverDESK resets and
updates the post-signup success panel to redirect to SilverDESK login with
the username pre-populated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-22 21:16:13 +00:00
parent c4febd7036
commit 9cbbd2d4f2
6 changed files with 749 additions and 25 deletions

View File

@@ -48,10 +48,116 @@ public static class DeveloperEndpoints
if (string.IsNullOrEmpty(fullName) || string.IsNullOrEmpty(email) || string.IsNullOrEmpty(desiredUsername))
return Results.Problem("Could not parse applicant details from ticket description", statusCode: 422);
var (success, message) = await provisioningService.ApproveApplicationAsync(
ticketId, desiredUsername, email, fullName);
// Generate confirmation token instead of provisioning immediately
var deployment = provisioningService.CreatePendingDeployment(desiredUsername, email, fullName, ticketId);
var siteBase = config["SiteBaseUrl"] ?? "https://silverlabs.uk";
var confirmUrl = $"{siteBase}/developers/confirm/{deployment.Token}";
// Send ticket reply with confirmation link
var replyContent = $"""
Your application has been approved! To activate your accounts, please confirm your identity:
**[Click here to activate your accounts]({confirmUrl})**
You'll need to enter your SilverDESK password to complete the setup. This link expires in 48 hours.
Once confirmed, the following accounts will be created for you:
- **Email**: {desiredUsername}@silverlabs.uk
- **Mattermost**: Team chat access
- **Gitea**: Source code repository access
""";
var (replyOk, replyMsg) = await provisioningService.SendTicketReplyAsync(ticketId, replyContent);
return Results.Ok(new
{
success = true,
message = $"Confirmation link generated and sent via ticket reply. Reply status: {replyMsg}",
confirmUrl
});
});
// Token info endpoint for the confirmation page
group.MapGet("/deployment-info/{token}", (string token, ProvisioningService provisioningService) =>
{
var deployment = provisioningService.GetPendingDeployment(token);
if (deployment is null)
return Results.NotFound(new { message = "Invalid or expired confirmation link" });
return Results.Ok(new
{
username = deployment.Username,
email = deployment.Email,
fullName = deployment.FullName,
expiresAt = deployment.ExpiresAt
});
});
// Confirm deployment with password
group.MapPost("/confirm-deployment", async (
ConfirmDeploymentRequest request,
ProvisioningService provisioningService) =>
{
var deployment = provisioningService.GetPendingDeployment(request.Token);
if (deployment is null)
return Results.NotFound(new { message = "Invalid or expired confirmation link" });
// Validate credentials against SilverDESK
var authenticated = await provisioningService.ValidateSilverDeskCredentialsAsync(
deployment.Username, request.Password);
if (!authenticated)
return Results.Json(new { message = "Invalid password. Please enter your SilverDESK password." }, statusCode: 401);
// Provision all services with the user's password
var (success, message) = await provisioningService.ProvisionWithPasswordAsync(
deployment.TicketId, deployment.Username, deployment.Email, deployment.FullName, request.Password);
// Send follow-up ticket reply with results
var resultContent = success
? $"""
Your accounts have been successfully provisioned:
{message}
You can now log in to all services with your SilverDESK credentials.
"""
: $"""
Account provisioning completed with some issues:
{message}
Please contact an administrator if you have trouble accessing any services.
""";
await provisioningService.SendTicketReplyAsync(deployment.TicketId, resultContent, "close");
// Remove the used token
provisioningService.RemovePendingDeployment(request.Token);
return Results.Ok(new { success, message });
});
// Password sync endpoint (called by SilverDESK on password reset)
group.MapPost("/sync-password", async (
SyncPasswordRequest request,
ProvisioningService provisioningService,
HttpContext context,
IConfiguration config) =>
{
var apiKey = context.Request.Headers["X-Api-Key"].FirstOrDefault();
var expectedKey = config["AdminApiKey"];
if (string.IsNullOrEmpty(expectedKey) || apiKey != expectedKey)
return Results.Unauthorized();
var (success, message) = await provisioningService.SyncPasswordAsync(request.Username, request.NewPassword);
return Results.Ok(new { success, message });
});
}
}
public record ConfirmDeploymentRequest(string Token, string Password);
public record SyncPasswordRequest(string Username, string NewPassword);