Allow custom admin username and email during install
The installer now prompts for and validates a custom admin username and email, updating migrations and SQL placeholders accordingly. Login now accepts either username or email, and the login form and installer views have been updated to reflect these changes. Additional logging and migration handling improvements were made for better installation and authentication workflows.
This commit is contained in:
@@ -51,7 +51,7 @@ class AuthController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$username = trim($_POST['username'] ?? '');
|
$username = trim($_POST['username'] ?? '');
|
||||||
$password = $_POST['password'] ?? '';
|
$password = $_POST['password'] ?? ''; // Don't trim - passwords may have intentional spaces
|
||||||
$remember = isset($_POST['remember']);
|
$remember = isset($_POST['remember']);
|
||||||
|
|
||||||
// Validate input
|
// Validate input
|
||||||
@@ -61,10 +61,19 @@ class AuthController extends Controller
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find user
|
// Find user by username or email
|
||||||
$user = $this->userModel->findByUsername($username);
|
$user = $this->userModel->findByUsername($username);
|
||||||
|
|
||||||
|
// If not found by username, try email
|
||||||
|
if (!$user && filter_var($username, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$users = $this->userModel->where('email', $username);
|
||||||
|
if (!empty($users) && $users[0]['is_active']) {
|
||||||
|
$user = $users[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
|
error_log("Login failed: User '$username' not found or not active");
|
||||||
$_SESSION['error'] = 'Invalid username or password';
|
$_SESSION['error'] = 'Invalid username or password';
|
||||||
$this->redirect('/login');
|
$this->redirect('/login');
|
||||||
return;
|
return;
|
||||||
@@ -72,11 +81,15 @@ class AuthController extends Controller
|
|||||||
|
|
||||||
// Verify password
|
// Verify password
|
||||||
if (!$this->userModel->verifyPassword($password, $user['password'])) {
|
if (!$this->userModel->verifyPassword($password, $user['password'])) {
|
||||||
|
error_log("Login failed: Password verification failed for user '$username'");
|
||||||
|
error_log("Stored hash: {$user['password']}");
|
||||||
$_SESSION['error'] = 'Invalid username or password';
|
$_SESSION['error'] = 'Invalid username or password';
|
||||||
$this->redirect('/login');
|
$this->redirect('/login');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
error_log("Login successful for user '$username'");
|
||||||
|
|
||||||
// Check if email verification is required
|
// Check if email verification is required
|
||||||
$requireVerification = $this->settingModel->getValue('require_email_verification');
|
$requireVerification = $this->settingModel->getValue('require_email_verification');
|
||||||
if ($requireVerification && !$user['email_verified'] && $user['role'] !== 'admin') {
|
if ($requireVerification && !$user['email_verified'] && $user['role'] !== 'admin') {
|
||||||
|
|||||||
@@ -175,10 +175,17 @@ class InstallerController extends Controller
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$adminUsername = trim($_POST['admin_username'] ?? '');
|
||||||
$adminPassword = trim($_POST['admin_password'] ?? '');
|
$adminPassword = trim($_POST['admin_password'] ?? '');
|
||||||
$adminEmail = trim($_POST['admin_email'] ?? '');
|
$adminEmail = trim($_POST['admin_email'] ?? '');
|
||||||
|
|
||||||
// Validate
|
// Validate
|
||||||
|
if (empty($adminUsername) || !preg_match('/^[a-zA-Z0-9_]+$/', $adminUsername)) {
|
||||||
|
$_SESSION['error'] = 'Username can only contain letters, numbers, and underscores';
|
||||||
|
$this->redirect('/install');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (empty($adminPassword) || strlen($adminPassword) < 8) {
|
if (empty($adminPassword) || strlen($adminPassword) < 8) {
|
||||||
$_SESSION['error'] = 'Admin password must be at least 8 characters';
|
$_SESSION['error'] = 'Admin password must be at least 8 characters';
|
||||||
$this->redirect('/install');
|
$this->redirect('/install');
|
||||||
@@ -204,10 +211,12 @@ class InstallerController extends Controller
|
|||||||
|
|
||||||
$sql = file_get_contents($file);
|
$sql = file_get_contents($file);
|
||||||
|
|
||||||
// Replace password placeholder for user migration
|
// Replace placeholders for user migration or consolidated schema
|
||||||
if ($migration === '002_create_users_table.sql') {
|
if ($migration === '002_create_users_table.sql' || $migration === '000_initial_schema_v1.1.0.sql') {
|
||||||
$passwordHash = password_hash($adminPassword, PASSWORD_BCRYPT);
|
$passwordHash = password_hash($adminPassword, PASSWORD_BCRYPT);
|
||||||
$sql = str_replace('{{ADMIN_PASSWORD_HASH}}', $passwordHash, $sql);
|
$sql = str_replace('{{ADMIN_PASSWORD_HASH}}', $passwordHash, $sql);
|
||||||
|
$sql = str_replace('{{ADMIN_USERNAME}}', $adminUsername, $sql);
|
||||||
|
$sql = str_replace('{{ADMIN_EMAIL}}', $adminEmail, $sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute SQL
|
// Execute SQL
|
||||||
@@ -233,9 +242,33 @@ class InstallerController extends Controller
|
|||||||
$results[] = $migration;
|
$results[] = $migration;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update admin email and ensure admin role and verified status
|
// If using consolidated schema, mark all individual migrations as executed too
|
||||||
$stmt = $pdo->prepare("UPDATE users SET email = ?, role = 'admin', email_verified = 1 WHERE username = 'admin'");
|
if (in_array('000_initial_schema_v1.1.0.sql', $migrations)) {
|
||||||
$stmt->execute([$adminEmail]);
|
$allMigrations = [
|
||||||
|
'001_create_tables.sql',
|
||||||
|
'002_create_users_table.sql',
|
||||||
|
'003_add_whois_fields.sql',
|
||||||
|
'004_create_tld_registry_table.sql',
|
||||||
|
'005_update_tld_import_logs.sql',
|
||||||
|
'006_add_complete_workflow_import_type.sql',
|
||||||
|
'007_add_app_and_email_settings.sql',
|
||||||
|
'008_add_notes_to_domains.sql',
|
||||||
|
'009_add_authentication_features.sql',
|
||||||
|
'010_add_app_version_setting.sql',
|
||||||
|
'011_create_sessions_table.sql',
|
||||||
|
'012_link_remember_tokens_to_sessions.sql',
|
||||||
|
'013_create_user_notifications_table.sql'
|
||||||
|
];
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO migrations (migration) VALUES (?) ON DUPLICATE KEY UPDATE migration=migration");
|
||||||
|
foreach ($allMigrations as $individualMigration) {
|
||||||
|
$stmt->execute([$individualMigration]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update admin user to ensure role and verified status (in case migration already had defaults)
|
||||||
|
$stmt = $pdo->prepare("UPDATE users SET role = 'admin', email_verified = 1 WHERE username = ?");
|
||||||
|
$stmt->execute([$adminUsername]);
|
||||||
|
|
||||||
// Generate encryption key if not exists
|
// Generate encryption key if not exists
|
||||||
if (empty($_ENV['APP_ENCRYPTION_KEY'])) {
|
if (empty($_ENV['APP_ENCRYPTION_KEY'])) {
|
||||||
@@ -249,12 +282,13 @@ class InstallerController extends Controller
|
|||||||
// Create welcome notification for admin
|
// Create welcome notification for admin
|
||||||
try {
|
try {
|
||||||
// Get the admin user ID
|
// Get the admin user ID
|
||||||
$stmt = $pdo->query("SELECT id FROM users WHERE username = 'admin' LIMIT 1");
|
$stmt = $pdo->prepare("SELECT id FROM users WHERE username = ? LIMIT 1");
|
||||||
|
$stmt->execute([$adminUsername]);
|
||||||
$adminUser = $stmt->fetch(\PDO::FETCH_ASSOC);
|
$adminUser = $stmt->fetch(\PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
if ($adminUser) {
|
if ($adminUser) {
|
||||||
$notificationService = new \App\Services\NotificationService();
|
$notificationService = new \App\Services\NotificationService();
|
||||||
$notificationService->notifyWelcome($adminUser['id'], 'admin');
|
$notificationService->notifyWelcome($adminUser['id'], $adminUsername);
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
// Don't fail install if notification fails
|
// Don't fail install if notification fails
|
||||||
@@ -263,6 +297,7 @@ class InstallerController extends Controller
|
|||||||
|
|
||||||
// Redirect to complete page
|
// Redirect to complete page
|
||||||
$_SESSION['install_complete'] = true;
|
$_SESSION['install_complete'] = true;
|
||||||
|
$_SESSION['admin_username'] = $adminUsername;
|
||||||
$_SESSION['admin_password'] = $adminPassword;
|
$_SESSION['admin_password'] = $adminPassword;
|
||||||
$this->redirect('/install/complete');
|
$this->redirect('/install/complete');
|
||||||
|
|
||||||
@@ -385,12 +420,15 @@ class InstallerController extends Controller
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$adminUsername = $_SESSION['admin_username'] ?? 'admin';
|
||||||
$adminPassword = $_SESSION['admin_password'] ?? null;
|
$adminPassword = $_SESSION['admin_password'] ?? null;
|
||||||
|
unset($_SESSION['admin_username']);
|
||||||
unset($_SESSION['admin_password']);
|
unset($_SESSION['admin_password']);
|
||||||
unset($_SESSION['install_complete']);
|
unset($_SESSION['install_complete']);
|
||||||
|
|
||||||
$this->view('installer/complete', [
|
$this->view('installer/complete', [
|
||||||
'title' => 'Installation Complete',
|
'title' => 'Installation Complete',
|
||||||
|
'adminUsername' => $adminUsername,
|
||||||
'adminPassword' => $adminPassword
|
'adminPassword' => $adminPassword
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ ob_start();
|
|||||||
<!-- Username Field -->
|
<!-- Username Field -->
|
||||||
<div>
|
<div>
|
||||||
<label for="username" class="block text-sm font-medium text-gray-700 mb-1.5">
|
<label for="username" class="block text-sm font-medium text-gray-700 mb-1.5">
|
||||||
Username
|
Username or Email
|
||||||
</label>
|
</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
@@ -41,7 +41,7 @@ ob_start();
|
|||||||
required
|
required
|
||||||
autofocus
|
autofocus
|
||||||
class="w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
|
class="w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm"
|
||||||
placeholder="Enter your username">
|
placeholder="Enter your username or email">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-sm font-medium text-gray-600">Username:</span>
|
<span class="text-sm font-medium text-gray-600">Username:</span>
|
||||||
<span class="text-sm font-mono font-bold text-gray-900">admin</span>
|
<span class="text-sm font-mono font-bold text-gray-900 select-all"><?= htmlspecialchars($adminUsername ?? 'admin') ?></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-sm font-medium text-gray-600">Password:</span>
|
<span class="text-sm font-medium text-gray-600">Password:</span>
|
||||||
|
|||||||
@@ -78,6 +78,21 @@
|
|||||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Administrator Account</h3>
|
<h3 class="text-lg font-semibold text-gray-900 mb-4">Administrator Account</h3>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="admin_username" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Username <span class="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<i class="fas fa-user text-gray-400 text-sm"></i>
|
||||||
|
</div>
|
||||||
|
<input type="text" id="admin_username" name="admin_username" required pattern="[a-zA-Z0-9_]+"
|
||||||
|
class="w-full pl-10 pr-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary"
|
||||||
|
placeholder="admin" value="admin">
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">Letters, numbers, and underscores only</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="admin_email" class="block text-sm font-medium text-gray-700 mb-2">
|
<label for="admin_email" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
Email Address <span class="text-red-500">*</span>
|
Email Address <span class="text-red-500">*</span>
|
||||||
|
|||||||
@@ -5,6 +5,15 @@
|
|||||||
-- CORE TABLES
|
-- CORE TABLES
|
||||||
-- =====================================================
|
-- =====================================================
|
||||||
|
|
||||||
|
-- Notification groups table (must be created first - referenced by domains)
|
||||||
|
CREATE TABLE IF NOT EXISTS notification_groups (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- Domains table
|
-- Domains table
|
||||||
CREATE TABLE IF NOT EXISTS domains (
|
CREATE TABLE IF NOT EXISTS domains (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
@@ -30,15 +39,6 @@ CREATE TABLE IF NOT EXISTS domains (
|
|||||||
INDEX idx_is_active (is_active)
|
INDEX idx_is_active (is_active)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- Notification groups table
|
|
||||||
CREATE TABLE IF NOT EXISTS notification_groups (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
name VARCHAR(255) NOT NULL,
|
|
||||||
description TEXT,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- Notification channels table
|
-- Notification channels table
|
||||||
CREATE TABLE IF NOT EXISTS notification_channels (
|
CREATE TABLE IF NOT EXISTS notification_channels (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
@@ -92,9 +92,9 @@ CREATE TABLE IF NOT EXISTS users (
|
|||||||
INDEX idx_role (role)
|
INDEX idx_role (role)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- Insert default admin user (password will be set during installation)
|
-- Insert default admin user (credentials will be set during installation)
|
||||||
INSERT INTO users (username, password, email, full_name, is_active, role, email_verified) VALUES
|
INSERT INTO users (username, password, email, full_name, is_active, role, email_verified) VALUES
|
||||||
('admin', '{{ADMIN_PASSWORD_HASH}}', 'admin@domainmonitor.local', 'Administrator', 1, 'admin', 1)
|
('{{ADMIN_USERNAME}}', '{{ADMIN_PASSWORD_HASH}}', '{{ADMIN_EMAIL}}', 'Administrator', 1, 'admin', 1)
|
||||||
ON DUPLICATE KEY UPDATE username=username;
|
ON DUPLICATE KEY UPDATE username=username;
|
||||||
|
|
||||||
-- Password reset tokens table
|
-- Password reset tokens table
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ CREATE TABLE IF NOT EXISTS users (
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- Insert default admin user
|
-- Insert default admin user
|
||||||
-- Password is randomly generated during installation and displayed in output
|
-- Credentials are set during installation and displayed in output
|
||||||
-- Hash placeholder will be replaced by web installer
|
-- Placeholders will be replaced by web installer
|
||||||
INSERT INTO users (username, password, email, full_name, is_active) VALUES
|
INSERT INTO users (username, password, email, full_name, is_active) VALUES
|
||||||
('admin', '{{ADMIN_PASSWORD_HASH}}', 'admin@domainmonitor.local', 'Administrator', 1)
|
('{{ADMIN_USERNAME}}', '{{ADMIN_PASSWORD_HASH}}', '{{ADMIN_EMAIL}}', 'Administrator', 1)
|
||||||
ON DUPLICATE KEY UPDATE username=username;
|
ON DUPLICATE KEY UPDATE username=username;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user