Sovrant
-
Sign in to continue.
+ @if (_firstRun)
+ {
+
Welcome! Let's set up your server.
+
+ First-time setup — no accounts exist yet. The first account you create
+ becomes the administrator , with full control over registration,
+ approvals, and server settings.
+
+ }
+ else
+ {
+
Sign in to continue.
+ }
@if (_errorMessage is not null)
{
@_errorMessage
}
+ @if (_infoMessage is not null)
+ {
+
@_infoMessage
+ }
+
Email
-
- @(_busy ? "Signing in…" : "Sign in")
-
-
- @if (_registrationOpen)
+ @if (_firstRun)
+ {
+
+ Create administrator account
+
+ }
+ else
{
-
-
Don't have an account?
-
- @(_busy ? "Creating account…" : "Create account")
+
+ Sign in
+
+ @if (_registrationOpen)
+ {
+
+ Don't have an account?
+
+ Create account
+
+ @if (_approvalRequired)
+ {
+ New accounts require administrator approval before first sign-in.
+ }
+ }
}
@if (_busy)
{
+
+
+ @_busyLabel
+
}
@@ -66,8 +100,12 @@
private string _email = string.Empty;
private string _password = string.Empty;
private string? _errorMessage;
+ private string? _infoMessage;
private bool _busy;
+ private string _busyLabel = string.Empty;
private bool _registrationOpen;
+ private bool _firstRun;
+ private bool _approvalRequired;
protected override async Task OnInitializedAsync()
{
@@ -76,23 +114,37 @@
Nav.NavigateTo("/", forceLoad: true);
return;
}
+ _firstRun = await IdentityService.IsFirstRunAsync().ConfigureAwait(false);
_registrationOpen = await IdentityService.IsRegistrationOpenAsync().ConfigureAwait(false);
+ _approvalRequired = !_firstRun && _registrationOpen
+ && await IdentityService.IsApprovalRequiredAsync().ConfigureAwait(false);
}
private async Task OnKeyDown(KeyboardEventArgs e)
{
- if (e.Key == "Enter") await LoginAsync();
+ if (e.Key != "Enter") return;
+ // On first run there is nobody to sign in as yet — Enter creates the admin account.
+ if (_firstRun) await RegisterAsync();
+ else await LoginAsync();
}
- private async Task LoginAsync()
+ private bool ValidateInput()
{
_errorMessage = null;
+ _infoMessage = null;
if (string.IsNullOrWhiteSpace(_email) || string.IsNullOrWhiteSpace(_password))
{
_errorMessage = "Email and password are required.";
- return;
+ return false;
}
+ return true;
+ }
+
+ private async Task LoginAsync()
+ {
+ if (!ValidateInput()) return;
_busy = true;
+ _busyLabel = "Signing you in…";
try
{
var result = await IdentityService.LoginAsync(_email, _password).ConfigureAwait(false);
@@ -120,13 +172,9 @@
private async Task RegisterAsync()
{
- _errorMessage = null;
- if (string.IsNullOrWhiteSpace(_email) || string.IsNullOrWhiteSpace(_password))
- {
- _errorMessage = "Email and password are required.";
- return;
- }
+ if (!ValidateInput()) return;
_busy = true;
+ _busyLabel = _firstRun ? "Creating your administrator account…" : "Creating your account…";
try
{
var result = await IdentityService.RegisterAsync(_email, _password).ConfigureAwait(false);
@@ -137,7 +185,7 @@
}
if (result.IsPendingApproval)
{
- _errorMessage = "Account created. An administrator must approve it before you can sign in.";
+ _infoMessage = "Account created. An administrator must approve it before you can sign in.";
return;
}
await CredentialStore.StoreAsync(StoredTokenKey, result.Token!).ConfigureAwait(false);
diff --git a/src/Sovrant.Web/wwwroot/css/sovrant.css b/src/Sovrant.Web/wwwroot/css/sovrant.css
index 829c2b41..d255052f 100644
--- a/src/Sovrant.Web/wwwroot/css/sovrant.css
+++ b/src/Sovrant.Web/wwwroot/css/sovrant.css
@@ -3450,6 +3450,12 @@ details[open] .doc-advanced-toggle::before { content: "▼ "; }
.login-sub { font-size: 13px; color: var(--text-secondary); text-align: center; }
.login-progress { height: 3px; background: var(--brand-primary); border-radius: 2px; animation: login-pulse 1s ease-in-out infinite; }
@keyframes login-pulse { 0%, 100% { opacity: 0.4; } 50% { opacity: 1; } }
+.login-info { font-size: 13px; line-height: 1.5; color: var(--text-primary); background: color-mix(in srgb, var(--brand-primary) 10%, transparent); border: 1px solid color-mix(in srgb, var(--brand-primary) 35%, transparent); padding: 10px 12px; border-radius: 5px; }
+.login-success { font-size: 13px; line-height: 1.5; color: var(--status-pass); background: color-mix(in srgb, var(--status-pass) 10%, transparent); border: 1px solid color-mix(in srgb, var(--status-pass) 30%, transparent); padding: 8px 10px; border-radius: 5px; }
+.login-note { font-size: 12px; color: var(--text-secondary); text-align: center; }
+.login-status { display: flex; align-items: center; justify-content: center; gap: 8px; font-size: 13px; color: var(--text-secondary); }
+.login-spinner { width: 14px; height: 14px; flex: none; border: 2px solid var(--surface-border); border-top-color: var(--brand-primary); border-radius: 50%; animation: login-spin 0.8s linear infinite; }
+@keyframes login-spin { to { transform: rotate(360deg); } }
/* ===== Admin workspace configure panel ===== */
.admin-config-panel { margin-top: 16px; padding: 16px; background: var(--surface-card); border: 1px solid var(--brand-primary); border-radius: 10px; display: flex; flex-direction: column; gap: 12px; }