Files
Website/BlazorApp/Endpoints/DeveloperEndpoints.cs
SysAdmin 9cbbd2d4f2
All checks were successful
Build and Deploy / deploy (push) Successful in 41s
feat(developers): add password-synced provisioning and deployment confirmation flow
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>
2026-02-22 21:16:13 +00:00

164 lines
7.1 KiB
C#

using SilverLabs.Website.Models;
using SilverLabs.Website.Services;
namespace SilverLabs.Website.Endpoints;
public static class DeveloperEndpoints
{
public static void MapDeveloperEndpoints(this WebApplication app)
{
var group = app.MapGroup("/api/developers");
group.MapGet("/check-username/{username}", async (string username, DeveloperApplicationService service) =>
{
var available = await service.CheckUsernameAsync(username);
if (available is null)
return Results.Problem("Unable to verify username availability", statusCode: 503);
return Results.Ok(new { available = available.Value });
});
group.MapPost("/apply", async (DeveloperApplication application, DeveloperApplicationService service) =>
{
var (success, message, token) = await service.SubmitApplicationAsync(application);
return success
? Results.Ok(new { message, token })
: Results.Problem(message, statusCode: 502);
});
group.MapPost("/approve/{ticketId}", async (
string ticketId,
DeveloperTicketParsingService ticketService,
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 ticket = await ticketService.FetchTicketAsync(ticketId);
if (ticket is null)
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);
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 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);