feat(developers): add password-synced provisioning and deployment confirmation flow
All checks were successful
Build and Deploy / deploy (push) Successful in 41s
All checks were successful
Build and Deploy / deploy (push) Successful in 41s
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>
This commit is contained in:
308
BlazorApp/Components/Pages/DeploymentConfirm.razor
Normal file
308
BlazorApp/Components/Pages/DeploymentConfirm.razor
Normal file
@@ -0,0 +1,308 @@
|
||||
@page "/developers/confirm/{Token}"
|
||||
@inject HttpClient Http
|
||||
@inject NavigationManager Navigation
|
||||
@inject IConfiguration Configuration
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Activate Your Accounts - SilverLabs</PageTitle>
|
||||
|
||||
<div class="main-content visible">
|
||||
<header class="header">
|
||||
<img src="logo.png" alt="SilverLabs Logo" class="logo">
|
||||
</header>
|
||||
|
||||
<div class="dev-container">
|
||||
<div class="dev-header">
|
||||
<h1>Activate Your Accounts</h1>
|
||||
<p class="dev-subtitle">Confirm your identity to provision your SilverLabs developer accounts.</p>
|
||||
</div>
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
<div class="dev-section" style="text-align: center; padding: 3rem;">
|
||||
<div class="btn-spinner" style="width: 32px; height: 32px; margin: 0 auto 1rem;"></div>
|
||||
<p style="color: rgba(255,255,255,0.6);">Loading deployment details...</p>
|
||||
</div>
|
||||
}
|
||||
else if (_invalidToken)
|
||||
{
|
||||
<div class="dev-section" style="text-align: center; padding: 3rem;">
|
||||
<div class="confirm-icon confirm-icon-error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="15" y1="9" x2="9" y2="15"></line>
|
||||
<line x1="9" y1="9" x2="15" y2="15"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 style="color: #f87171; margin-bottom: 0.75rem;">Invalid or Expired Link</h2>
|
||||
<p style="color: rgba(255,255,255,0.6); max-width: 400px; margin: 0 auto;">This confirmation link is no longer valid. It may have expired or already been used. Please contact an administrator if you need a new link.</p>
|
||||
</div>
|
||||
}
|
||||
else if (_provisioned)
|
||||
{
|
||||
<div class="dev-success-panel">
|
||||
<div class="success-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
|
||||
<polyline points="22 4 12 14.01 9 11.01"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
<h2>Accounts Activated</h2>
|
||||
<p>@_resultMessage</p>
|
||||
<div class="confirm-services">
|
||||
<div class="confirm-service-item">
|
||||
<strong>Email</strong>
|
||||
<span>@(_username)@@silverlabs.uk</span>
|
||||
</div>
|
||||
<div class="confirm-service-item">
|
||||
<strong>Mattermost</strong>
|
||||
<span>Team chat & collaboration</span>
|
||||
</div>
|
||||
<div class="confirm-service-item">
|
||||
<strong>Gitea</strong>
|
||||
<span>Source code repositories</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="dev-account-note">All accounts use the same password you just entered.</p>
|
||||
<div class="dev-success-actions">
|
||||
<a href="https://silverdesk.silverlabs.uk" target="_blank" class="dev-btn dev-btn-primary">Go to SilverDESK</a>
|
||||
<a href="/" class="dev-btn dev-btn-secondary">Back to Home</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="dev-section">
|
||||
<h2 class="dev-section-title">Confirm Your Identity</h2>
|
||||
<p class="dev-section-desc">Enter your SilverDESK password to activate your accounts. All services will use this same password.</p>
|
||||
|
||||
<div class="confirm-user-info">
|
||||
<div class="confirm-user-field">
|
||||
<span class="confirm-label">Username</span>
|
||||
<span class="confirm-value">@_username</span>
|
||||
</div>
|
||||
<div class="confirm-user-field">
|
||||
<span class="confirm-label">Email</span>
|
||||
<span class="confirm-value">@_email</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-top: 1.5rem; max-width: 400px;">
|
||||
<label for="password">SilverDESK Password</label>
|
||||
<input id="password" type="password" class="form-input" @bind="_password"
|
||||
@bind:event="oninput" @onkeydown="HandleKeyDown" placeholder="Enter your password" />
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(_errorMessage))
|
||||
{
|
||||
<div class="dev-error" style="margin-top: 1rem;">@_errorMessage</div>
|
||||
}
|
||||
|
||||
<div style="margin-top: 1.5rem;">
|
||||
<button class="dev-btn dev-btn-primary" disabled="@_submitting" @onclick="HandleConfirm">
|
||||
@if (_submitting)
|
||||
{
|
||||
<span class="btn-spinner"></span>
|
||||
<span>Activating accounts...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Activate My Accounts</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<a href="/" class="back-link">← Back to SilverLabs Home</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.confirm-icon-error {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
margin: 0 auto 1.5rem;
|
||||
background: rgba(248, 113, 113, 0.15);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.confirm-icon-error svg {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
stroke: #f87171;
|
||||
}
|
||||
|
||||
.confirm-user-info {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.confirm-user-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.confirm-label {
|
||||
font-size: 0.78rem;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.confirm-value {
|
||||
font-size: 1rem;
|
||||
color: #4DD0E1;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.confirm-services {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
margin: 1.5rem auto;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.confirm-service-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.confirm-service-item strong {
|
||||
color: #4DD0E1;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.confirm-service-item span {
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
@@media (max-width: 768px) {
|
||||
.confirm-user-info {
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.confirm-services {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
[Parameter] public string Token { get; set; } = "";
|
||||
|
||||
private bool _loading = true;
|
||||
private bool _invalidToken;
|
||||
private bool _provisioned;
|
||||
private bool _submitting;
|
||||
private string? _username;
|
||||
private string? _email;
|
||||
private string? _password;
|
||||
private string? _errorMessage;
|
||||
private string? _resultMessage;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!firstRender) return;
|
||||
|
||||
try
|
||||
{
|
||||
var baseUrl = Navigation.BaseUri.TrimEnd('/');
|
||||
using var client = new HttpClient();
|
||||
var response = await client.GetAsync($"{baseUrl}/api/developers/deployment-info/{Token}");
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var data = await response.Content.ReadFromJsonAsync<DeploymentInfo>();
|
||||
_username = data?.Username;
|
||||
_email = data?.Email;
|
||||
}
|
||||
else
|
||||
{
|
||||
_invalidToken = true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
_invalidToken = true;
|
||||
}
|
||||
|
||||
_loading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task HandleKeyDown(KeyboardEventArgs e)
|
||||
{
|
||||
if (e.Key == "Enter" && !_submitting && !string.IsNullOrEmpty(_password))
|
||||
await HandleConfirm();
|
||||
}
|
||||
|
||||
private async Task HandleConfirm()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_password))
|
||||
{
|
||||
_errorMessage = "Please enter your password.";
|
||||
return;
|
||||
}
|
||||
|
||||
_errorMessage = null;
|
||||
_submitting = true;
|
||||
StateHasChanged();
|
||||
|
||||
try
|
||||
{
|
||||
var baseUrl = Navigation.BaseUri.TrimEnd('/');
|
||||
using var client = new HttpClient();
|
||||
var payload = new { token = Token, password = _password };
|
||||
var response = await client.PostAsJsonAsync($"{baseUrl}/api/developers/confirm-deployment", payload);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var result = await response.Content.ReadFromJsonAsync<ProvisionResult>();
|
||||
_resultMessage = result?.Message ?? "Accounts activated successfully.";
|
||||
_provisioned = true;
|
||||
}
|
||||
else if ((int)response.StatusCode == 401)
|
||||
{
|
||||
_errorMessage = "Incorrect password. Please enter the password you created when you applied.";
|
||||
}
|
||||
else if ((int)response.StatusCode == 404)
|
||||
{
|
||||
_invalidToken = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_errorMessage = "Something went wrong. Please try again or contact an administrator.";
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
_errorMessage = "Connection error. Please try again.";
|
||||
}
|
||||
finally
|
||||
{
|
||||
_submitting = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private record DeploymentInfo(string Username, string Email, string FullName, DateTime ExpiresAt);
|
||||
private record ProvisionResult(bool Success, string Message);
|
||||
}
|
||||
Reference in New Issue
Block a user