Files
Website/BlazorApp/Endpoints/DeveloperEndpoints.cs
SysAdmin 44e3ad94e0
All checks were successful
Build and Deploy / deploy (push) Successful in 41s
feat(developers): add service URLs and onboarding guide to provisioning reply and success page
Ticket replies now include full onboarding info (webmail, IMAP/SMTP, Mattermost, Gitea, SilverDESK URLs) instead of raw provisioning status. Confirmation success page uses clickable service links with email client config details.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 22:57:51 +00:00

181 lines
8.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! 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)
**Gitea** (Source Code): [git.silverlabs.uk](https://git.silverlabs.uk)
**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)
- **Gitea**: [git.silverlabs.uk](https://git.silverlabs.uk)
""";
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);