All checks were successful
Build and Deploy / deploy (push) Successful in 18s
New users are now added to the SilverLABS Mattermost team after account creation. Gitea provisioning is skipped for Testers (only Developers get repo access). Role is parsed from ticket description and threaded through the entire approval/confirmation flow. Gitea API token is now configured. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
189 lines
8.6 KiB
C#
189 lines
8.6 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, 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);
|