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, role) = 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, role); var siteBase = config["SiteBaseUrl"] ?? "https://silverlabs.uk"; var confirmUrl = $"{siteBase}/developers/confirm/{deployment.Token}"; // Send ticket reply with confirmation link var giteaLine = string.Equals(role, "Developer", StringComparison.OrdinalIgnoreCase) ? "\n- **Gitea**: Source code repository access" : ""; 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{giteaLine} """; 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, 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 var resultContent = success ? $""" Your accounts have been successfully provisioned! Here's how to access your services: **Email**: {deployment.Username}@silverlabs.uk - Webmail: [mail.silverlined.uk](https://mail.silverlined.uk) - IMAP: `mail.silverlined.uk:993` (SSL) - SMTP: `mail.silverlined.uk:465` (SSL) **Mattermost** (Team Chat): [ops.silverlined.uk](https://ops.silverlined.uk){giteaSuccessSection} **SilverDESK** (Support & Tickets): [silverdesk.silverlabs.uk](https://silverdesk.silverlabs.uk) All services use the same password you entered during activation. --- *Provisioning status: {message}* """ : $""" Account provisioning completed with some issues: {message} Some services may not be available yet. Please contact an administrator for assistance. Once resolved, your services will be: - **Email**: {deployment.Username}@silverlabs.uk — [mail.silverlined.uk](https://mail.silverlined.uk) - **Mattermost**: [ops.silverlined.uk](https://ops.silverlined.uk){giteaFailSection} """; 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);