Merge pull request 'create user' (#4) from configuration into master
Build and Push Docker Image to Gitea Container Registry / build-and-push (push) Successful in 1m33s

Reviewed-on: #4
This commit was merged in pull request #4.
This commit is contained in:
2026-07-24 12:03:03 +02:00
5 changed files with 209 additions and 60 deletions
+2 -2
View File
@@ -148,9 +148,9 @@ VALUES ('192.168.1.1', 'unifi-gw', 'other', 'YourSharedSecret123', 'UniFi Gatewa
- CA Certificate: `Do not validate` / `Unvalidated` (or install your custom CA if using custom EAP certs). - CA Certificate: `Do not validate` / `Unvalidated` (or install your custom CA if using custom EAP certs).
- Enter the **Username** and **Password** generated from the portal. - Enter the **Username** and **Password** generated from the portal.
3. **Admin Management**: 3. **Admin Management & First-Time Setup**:
- Access the Admin Dashboard at `http://<server-ip>:8080/admin`. - Access the Admin Dashboard at `http://<server-ip>:8080/admin`.
- Default login: `admin` / `admin123`. - **First-Time Setup**: On initial startup, accessing the admin panel automatically presents the setup screen to create your custom administrator username and password.
- Monitor active RADIUS sessions, data usage, enforce data limits, or revoke users. - Monitor active RADIUS sessions, data usage, enforce data limits, or revoke users.
--- ---
@@ -8,62 +8,138 @@
<div class="brand-icon" style="margin: 0 auto 1rem auto; width: 60px; height: 60px; font-size: 1.8rem; background: linear-gradient(135deg, var(--accent-purple), var(--primary));"> <div class="brand-icon" style="margin: 0 auto 1rem auto; width: 60px; height: 60px; font-size: 1.8rem; background: linear-gradient(135deg, var(--accent-purple), var(--primary));">
<i class="bi bi-shield-lock-fill"></i> <i class="bi bi-shield-lock-fill"></i>
</div> </div>
<h1 class="portal-title">Administrator Login</h1> @if (isFirstRun)
<p class="portal-subtitle">Access RADIUS server policies, session limits, and data accounting</p> {
<h1 class="portal-title">First-Time Setup</h1>
<p class="portal-subtitle">Create your primary administrator account to secure the RADIUS Controller panel</p>
}
else
{
<h1 class="portal-title">Administrator Login</h1>
<p class="portal-subtitle">Access RADIUS server policies, session limits, and data accounting</p>
}
</div> </div>
<div class="glass-card"> <div class="glass-card">
<EditForm Model="this" OnValidSubmit="HandleLogin"> @if (isCheckingFirstRun)
<div class="form-group"> {
<label class="form-label" for="adminUsername">Username</label> <div style="text-align: center; padding: 2rem;">
<input id="adminUsername" <div class="spinner-border text-primary" role="status"></div>
type="text" <p style="margin-top: 1rem; color: var(--text-muted); font-size: 0.9rem;">Checking system configuration...</p>
class="form-control-glass" </div>
placeholder="admin" }
@bind="username" else if (isFirstRun)
required /> {
<div style="background: rgba(20, 184, 166, 0.1); border: 1px solid rgba(20, 184, 166, 0.3); border-radius: 8px; padding: 0.85rem 1rem; margin-bottom: 1.5rem; font-size: 0.85rem; color: var(--accent-teal);">
<i class="bi bi-info-circle-fill"></i> <strong>Welcome!</strong> No admin user exists in the database. Please create your administrator credentials below.
</div> </div>
<div class="form-group"> <EditForm Model="this" OnValidSubmit="HandleFirstTimeSetup">
<label class="form-label" for="adminPassword">Password</label> <div class="form-group">
<input id="adminPassword" <label class="form-label" for="setupUsername">Administrator Username</label>
type="password" <input id="setupUsername"
class="form-control-glass" type="text"
placeholder="••••••••" class="form-control-glass"
@bind="password" placeholder="e.g. admin"
required /> @bind="username"
</div> required />
@if (!string.IsNullOrEmpty(errorMessage))
{
<div style="color: var(--accent-rose); font-size: 0.875rem; margin-bottom: 1rem;">
<i class="bi bi-exclamation-triangle-fill"></i> @errorMessage
</div> </div>
}
<button type="submit" class="btn-gradient-primary" disabled="@isSubmitting"> <div class="form-group">
@if (isSubmitting) <label class="form-label" for="setupPassword">Password</label>
{ <input id="setupPassword"
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> type="password"
<span>Authenticating...</span> class="form-control-glass"
} placeholder="••••••••"
else @bind="password"
{ required />
<i class="bi bi-box-arrow-in-right"></i> </div>
<span>Sign In to Admin Panel</span>
}
</button>
</EditForm>
<div style="margin-top: 1.5rem; text-align: center; padding-top: 1rem; border-top: 1px solid var(--bg-card-border); font-size: 0.8rem; color: var(--text-dim);"> <div class="form-group">
Default credentials: <code style="color: var(--accent-teal);">admin</code> / <code style="color: var(--accent-teal);">admin123</code> <label class="form-label" for="confirmPassword">Confirm Password</label>
</div> <input id="confirmPassword"
type="password"
class="form-control-glass"
placeholder="••••••••"
@bind="confirmPassword"
required />
</div>
@if (!string.IsNullOrEmpty(errorMessage))
{
<div style="color: var(--accent-rose); font-size: 0.875rem; margin-bottom: 1rem;">
<i class="bi bi-exclamation-triangle-fill"></i> @errorMessage
</div>
}
<button type="submit" class="btn-gradient-primary" disabled="@isSubmitting">
@if (isSubmitting)
{
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span>Creating Administrator Account...</span>
}
else
{
<i class="bi bi-person-check-fill"></i>
<span>Create Admin Account & Log In</span>
}
</button>
</EditForm>
}
else
{
<EditForm Model="this" OnValidSubmit="HandleLogin">
<div class="form-group">
<label class="form-label" for="adminUsername">Username</label>
<input id="adminUsername"
type="text"
class="form-control-glass"
placeholder="Username"
@bind="username"
required />
</div>
<div class="form-group">
<label class="form-label" for="adminPassword">Password</label>
<input id="adminPassword"
type="password"
class="form-control-glass"
placeholder="••••••••"
@bind="password"
required />
</div>
@if (!string.IsNullOrEmpty(errorMessage))
{
<div style="color: var(--accent-rose); font-size: 0.875rem; margin-bottom: 1rem;">
<i class="bi bi-exclamation-triangle-fill"></i> @errorMessage
</div>
}
<button type="submit" class="btn-gradient-primary" disabled="@isSubmitting">
@if (isSubmitting)
{
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<span>Authenticating...</span>
}
else
{
<i class="bi bi-box-arrow-in-right"></i>
<span>Sign In to Admin Panel</span>
}
</button>
</EditForm>
}
</div> </div>
</div> </div>
@code { @code {
private string username = string.Empty; private string username = string.Empty;
private string password = string.Empty; private string password = string.Empty;
private string confirmPassword = string.Empty;
private bool isCheckingFirstRun = true;
private bool isFirstRun = false;
private bool isSubmitting = false; private bool isSubmitting = false;
private string errorMessage = string.Empty; private string errorMessage = string.Empty;
@@ -73,7 +149,60 @@
if (AuthService.IsAuthenticated) if (AuthService.IsAuthenticated)
{ {
NavManager.NavigateTo("/admin"); NavManager.NavigateTo("/admin");
return;
} }
try
{
isFirstRun = !await AuthService.HasAnyAdminAsync();
}
catch
{
isFirstRun = false;
}
finally
{
isCheckingFirstRun = false;
}
}
private async Task HandleFirstTimeSetup()
{
isSubmitting = true;
errorMessage = string.Empty;
if (string.IsNullOrWhiteSpace(username) || username.Trim().Length < 3)
{
errorMessage = "Username must be at least 3 characters long.";
isSubmitting = false;
return;
}
if (string.IsNullOrWhiteSpace(password) || password.Length < 6)
{
errorMessage = "Password must be at least 6 characters long.";
isSubmitting = false;
return;
}
if (password != confirmPassword)
{
errorMessage = "Password and Confirm Password do not match.";
isSubmitting = false;
return;
}
bool success = await AuthService.RegisterInitialAdminAsync(username, password);
if (success)
{
NavManager.NavigateTo("/admin");
}
else
{
errorMessage = "Failed to create administrator account. An admin user may already exist.";
}
isSubmitting = false;
} }
private async Task HandleLogin() private async Task HandleLogin()
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage; using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using radiuscontroller.Data; using radiuscontroller.Data;
using radiuscontroller.Models;
namespace radiuscontroller.Services; namespace radiuscontroller.Services;
@@ -39,6 +40,39 @@ public class AdminAuthService
} }
} }
public async Task<bool> HasAnyAdminAsync()
{
return await _db.AdminUsers.AnyAsync();
}
public async Task<bool> RegisterInitialAdminAsync(string username, string password)
{
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
return false;
// Ensure first-time setup is only allowed if no admin users exist
if (await _db.AdminUsers.AnyAsync())
return false;
string cleanUsername = username.Trim().ToLowerInvariant();
string hash = BCrypt.Net.BCrypt.HashPassword(password.Trim());
var admin = new AdminUser
{
Username = cleanUsername,
PasswordHash = hash,
CreatedAt = DateTime.UtcNow
};
_db.AdminUsers.Add(admin);
await _db.SaveChangesAsync();
CurrentAdminUsername = admin.Username;
await _sessionStorage.SetAsync(SessionKey, admin.Username);
NotifyStateChanged();
return true;
}
public async Task<bool> LoginAsync(string username, string password) public async Task<bool> LoginAsync(string username, string password)
{ {
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password)) if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
@@ -331,18 +331,6 @@ public class RadiusService : IRadiusService
{ {
await _db.Database.EnsureCreatedAsync(); await _db.Database.EnsureCreatedAsync();
if (!await _db.AdminUsers.AnyAsync())
{
// Default admin user: admin / admin123
string hash = BCrypt.Net.BCrypt.HashPassword("admin123");
_db.AdminUsers.Add(new AdminUser
{
Username = "admin",
PasswordHash = hash,
CreatedAt = DateTime.UtcNow
});
}
if (!await _db.SystemSettings.AnyAsync()) if (!await _db.SystemSettings.AnyAsync())
{ {
_db.SystemSettings.Add(new SystemSetting { Key = "default_session_time", Value = "60", UpdatedAt = DateTime.UtcNow }); _db.SystemSettings.Add(new SystemSetting { Key = "default_session_time", Value = "60", UpdatedAt = DateTime.UtcNow });
+2 -4
View File
@@ -114,7 +114,5 @@ CREATE TABLE IF NOT EXISTS system_settings (
INSERT INTO system_settings (key, value) VALUES ('default_session_time', '60') ON CONFLICT (key) DO NOTHING; INSERT INTO system_settings (key, value) VALUES ('default_session_time', '60') ON CONFLICT (key) DO NOTHING;
INSERT INTO system_settings (key, value) VALUES ('default_data_limit', '500') ON CONFLICT (key) DO NOTHING; INSERT INTO system_settings (key, value) VALUES ('default_data_limit', '500') ON CONFLICT (key) DO NOTHING;
-- Seed Default Admin User: admin / admin123 (BCrypt hash) -- System settings initialized on startup
INSERT INTO admin_users (username, password_hash) -- Admin user is created by administrator during first startup via the web interface
VALUES ('admin', '$2a$11$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy')
ON CONFLICT (username) DO NOTHING;