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);