feat(developers): add password-synced provisioning and deployment confirmation flow
All checks were successful
Build and Deploy / deploy (push) Successful in 41s
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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user