Add error log management and bulk admin actions
Introduces error log tracking with new ErrorLog model, controller, views, and migration. Adds admin UI for viewing, resolving, and deleting errors. Implements bulk actions for users and notification groups, refactors domain filtering/pagination, and centralizes admin access checks using Auth::requireAdmin().
This commit is contained in:
@@ -12,13 +12,11 @@ class AuthController extends Controller
|
||||
{
|
||||
private User $userModel;
|
||||
private Setting $settingModel;
|
||||
private $db;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->userModel = new User();
|
||||
$this->settingModel = new Setting();
|
||||
$this->db = \Core\Database::getConnection();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -290,11 +288,8 @@ class AuthController extends Controller
|
||||
// Generate verification token
|
||||
$token = bin2hex(random_bytes(32));
|
||||
|
||||
// Save token to database
|
||||
$stmt = $this->db->prepare(
|
||||
"UPDATE users SET email_verification_token = ?, email_verification_sent_at = NOW() WHERE id = ?"
|
||||
);
|
||||
$stmt->execute([$token, $userId]);
|
||||
// Save token to database using model
|
||||
$this->userModel->updateEmailVerificationToken($userId, $token);
|
||||
|
||||
// Send verification email
|
||||
$this->sendVerificationEmail($email, $fullName, $token);
|
||||
@@ -303,9 +298,8 @@ class AuthController extends Controller
|
||||
$_SESSION['pending_verification_email'] = $email;
|
||||
$this->redirect('/verify-email');
|
||||
} else {
|
||||
// Mark as verified and log them in
|
||||
$stmt = $this->db->prepare("UPDATE users SET email_verified = 1 WHERE id = ?");
|
||||
$stmt->execute([$userId]);
|
||||
// Mark as verified and log them in using model
|
||||
$this->userModel->markEmailAsVerified($userId);
|
||||
|
||||
$_SESSION['success'] = 'Account created successfully! You can now log in.';
|
||||
$this->redirect('/login');
|
||||
@@ -344,11 +338,8 @@ class AuthController extends Controller
|
||||
private function verifyEmail($token)
|
||||
{
|
||||
try {
|
||||
$stmt = $this->db->prepare(
|
||||
"SELECT * FROM users WHERE email_verification_token = ? AND email_verified = 0"
|
||||
);
|
||||
$stmt->execute([$token]);
|
||||
$user = $stmt->fetch();
|
||||
// Find user by verification token using model
|
||||
$user = $this->userModel->findByVerificationToken($token);
|
||||
|
||||
if (!$user) {
|
||||
$this->view('auth/verify-email', [
|
||||
@@ -370,11 +361,8 @@ class AuthController extends Controller
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark email as verified
|
||||
$stmt = $this->db->prepare(
|
||||
"UPDATE users SET email_verified = 1, email_verification_token = NULL WHERE id = ?"
|
||||
);
|
||||
$stmt->execute([$user['id']]);
|
||||
// Mark email as verified using model
|
||||
$this->userModel->verifyEmailByToken($user['id']);
|
||||
|
||||
$this->view('auth/verify-email', [
|
||||
'title' => 'Email Verified',
|
||||
@@ -424,10 +412,8 @@ class AuthController extends Controller
|
||||
// Generate new verification token
|
||||
$token = bin2hex(random_bytes(32));
|
||||
|
||||
$stmt = $this->db->prepare(
|
||||
"UPDATE users SET email_verification_token = ?, email_verification_sent_at = NOW() WHERE id = ?"
|
||||
);
|
||||
$stmt->execute([$token, $user['id']]);
|
||||
// Update verification token using model
|
||||
$this->userModel->updateEmailVerificationToken($user['id'], $token);
|
||||
|
||||
// Send verification email
|
||||
$this->sendVerificationEmail($user['email'], $user['full_name'], $token);
|
||||
@@ -507,11 +493,8 @@ class AuthController extends Controller
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$expiresAt = date('Y-m-d H:i:s', strtotime('+1 hour'));
|
||||
|
||||
// Save token to database
|
||||
$stmt = $this->db->prepare(
|
||||
"INSERT INTO password_reset_tokens (user_id, token, expires_at) VALUES (?, ?, ?)"
|
||||
);
|
||||
$stmt->execute([$user['id'], $token, $expiresAt]);
|
||||
// Save token to database using model
|
||||
$this->userModel->createPasswordResetToken($user['id'], $token, $expiresAt);
|
||||
|
||||
// Send reset email
|
||||
$this->sendPasswordResetEmail($user['email'], $user['full_name'], $token);
|
||||
@@ -538,12 +521,8 @@ class AuthController extends Controller
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify token exists and is not expired
|
||||
$stmt = $this->db->prepare(
|
||||
"SELECT * FROM password_reset_tokens WHERE token = ? AND used = 0 AND expires_at > NOW()"
|
||||
);
|
||||
$stmt->execute([$token]);
|
||||
$resetToken = $stmt->fetch();
|
||||
// Verify token exists and is not expired using model
|
||||
$resetToken = $this->userModel->findPasswordResetToken($token);
|
||||
|
||||
if (!$resetToken) {
|
||||
$_SESSION['error'] = 'Invalid or expired reset link';
|
||||
@@ -612,12 +591,8 @@ class AuthController extends Controller
|
||||
}
|
||||
|
||||
try {
|
||||
// Verify token
|
||||
$stmt = $this->db->prepare(
|
||||
"SELECT * FROM password_reset_tokens WHERE token = ? AND used = 0 AND expires_at > NOW()"
|
||||
);
|
||||
$stmt->execute([$token]);
|
||||
$resetToken = $stmt->fetch();
|
||||
// Verify token using model
|
||||
$resetToken = $this->userModel->findPasswordResetToken($token);
|
||||
|
||||
if (!$resetToken) {
|
||||
$_SESSION['error'] = 'Invalid or expired reset link';
|
||||
@@ -628,9 +603,8 @@ class AuthController extends Controller
|
||||
// Update password
|
||||
$this->userModel->changePassword($resetToken['user_id'], $password);
|
||||
|
||||
// Mark token as used
|
||||
$stmt = $this->db->prepare("UPDATE password_reset_tokens SET used = 1 WHERE id = ?");
|
||||
$stmt->execute([$resetToken['id']]);
|
||||
// Mark token as used using model
|
||||
$this->userModel->markPasswordResetTokenAsUsed($resetToken['id']);
|
||||
|
||||
$_SESSION['success'] = 'Password reset successfully! You can now log in.';
|
||||
$this->redirect('/login');
|
||||
@@ -651,10 +625,8 @@ class AuthController extends Controller
|
||||
$expiresAt = date('Y-m-d H:i:s', strtotime('+30 days'));
|
||||
$sessionId = session_id();
|
||||
|
||||
$stmt = $this->db->prepare(
|
||||
"INSERT INTO remember_tokens (user_id, session_id, token, expires_at) VALUES (?, ?, ?, ?)"
|
||||
);
|
||||
$stmt->execute([$userId, $sessionId, $token, $expiresAt]);
|
||||
// Create remember token using model
|
||||
$this->userModel->createRememberToken($userId, $sessionId, $token, $expiresAt);
|
||||
|
||||
// Set cookie
|
||||
setcookie('remember_token', $token, [
|
||||
@@ -683,11 +655,8 @@ class AuthController extends Controller
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $this->db->prepare(
|
||||
"SELECT user_id FROM remember_tokens WHERE token = ? AND expires_at > NOW()"
|
||||
);
|
||||
$stmt->execute([$token]);
|
||||
$rememberToken = $stmt->fetch();
|
||||
// Find user by remember token using model
|
||||
$rememberToken = $this->userModel->findByRememberToken($token);
|
||||
|
||||
if ($rememberToken) {
|
||||
$user = $this->userModel->find($rememberToken['user_id']);
|
||||
@@ -817,8 +786,8 @@ class AuthController extends Controller
|
||||
$token = $_COOKIE['remember_token'];
|
||||
|
||||
try {
|
||||
$stmt = $this->db->prepare("DELETE FROM remember_tokens WHERE token = ?");
|
||||
$stmt->execute([$token]);
|
||||
// Delete remember token using model
|
||||
$this->userModel->deleteRememberToken($token);
|
||||
} catch (\Exception $e) {
|
||||
// Silently fail
|
||||
}
|
||||
|
||||
@@ -76,13 +76,12 @@ class DashboardController extends Controller
|
||||
$status['database'] = ['status' => 'offline', 'color' => 'red'];
|
||||
}
|
||||
|
||||
// Check WHOIS service (test with a known TLD)
|
||||
// Check TLD Registry (WHOIS service)
|
||||
try {
|
||||
$whoisService = new \App\Services\WhoisService();
|
||||
// Quick test - just check if we can discover TLD servers
|
||||
$tldModel = new \App\Models\TldRegistry();
|
||||
$testTld = $tldModel->find(1); // Get first TLD
|
||||
if ($testTld) {
|
||||
// Check if ANY TLDs exist in registry (not just id=1)
|
||||
$tldStats = $tldModel->getStatistics();
|
||||
if ($tldStats['total'] > 0) {
|
||||
$status['whois'] = ['status' => 'active', 'color' => 'green'];
|
||||
} else {
|
||||
$status['whois'] = ['status' => 'no data', 'color' => 'yellow'];
|
||||
|
||||
@@ -36,61 +36,20 @@ class DomainController extends Controller
|
||||
$notificationDays = $settingModel->getNotificationDays();
|
||||
$expiringThreshold = !empty($notificationDays) ? max($notificationDays) : 30;
|
||||
|
||||
// Get all domains with groups
|
||||
$domains = $this->domainModel->getAllWithGroups();
|
||||
// Prepare filters array
|
||||
$filters = [
|
||||
'search' => $search,
|
||||
'status' => $status,
|
||||
'group' => $groupId
|
||||
];
|
||||
|
||||
// Apply filters
|
||||
if (!empty($search)) {
|
||||
$domains = array_filter($domains, function($domain) use ($search) {
|
||||
return stripos($domain['domain_name'], $search) !== false ||
|
||||
stripos($domain['registrar'] ?? '', $search) !== false;
|
||||
});
|
||||
}
|
||||
|
||||
if (!empty($status)) {
|
||||
$domains = array_filter($domains, function($domain) use ($status, $expiringThreshold) {
|
||||
if ($status === 'expiring_soon') {
|
||||
// Check if domain expires within configured threshold
|
||||
if (!empty($domain['expiration_date'])) {
|
||||
$daysLeft = floor((strtotime($domain['expiration_date']) - time()) / 86400);
|
||||
return $daysLeft <= $expiringThreshold && $daysLeft >= 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return $domain['status'] === $status;
|
||||
});
|
||||
}
|
||||
|
||||
if (!empty($groupId)) {
|
||||
$domains = array_filter($domains, function($domain) use ($groupId) {
|
||||
return $domain['notification_group_id'] == $groupId;
|
||||
});
|
||||
}
|
||||
|
||||
// Get total count after filtering
|
||||
$totalDomains = count($domains);
|
||||
|
||||
// Apply sorting
|
||||
usort($domains, function($a, $b) use ($sortBy, $sortOrder) {
|
||||
$aVal = $a[$sortBy] ?? '';
|
||||
$bVal = $b[$sortBy] ?? '';
|
||||
|
||||
$comparison = strcasecmp($aVal, $bVal);
|
||||
return $sortOrder === 'desc' ? -$comparison : $comparison;
|
||||
});
|
||||
|
||||
// Calculate pagination
|
||||
$totalPages = ceil($totalDomains / $perPage);
|
||||
$page = min($page, max(1, $totalPages)); // Ensure page is within valid range
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
// Slice array for current page
|
||||
$paginatedDomains = array_slice($domains, $offset, $perPage);
|
||||
// Get filtered and paginated domains using model
|
||||
$result = $this->domainModel->getFilteredPaginated($filters, $sortBy, $sortOrder, $page, $perPage, $expiringThreshold);
|
||||
|
||||
$groups = $this->groupModel->all();
|
||||
|
||||
// Format domains for display
|
||||
$formattedDomains = \App\Helpers\DomainHelper::formatMultiple($paginatedDomains);
|
||||
$formattedDomains = \App\Helpers\DomainHelper::formatMultiple($result['domains']);
|
||||
|
||||
$this->view('domains/index', [
|
||||
'domains' => $formattedDomains,
|
||||
@@ -102,14 +61,7 @@ class DomainController extends Controller
|
||||
'sort' => $sortBy,
|
||||
'order' => $sortOrder
|
||||
],
|
||||
'pagination' => [
|
||||
'current_page' => $page,
|
||||
'per_page' => $perPage,
|
||||
'total' => $totalDomains,
|
||||
'total_pages' => $totalPages,
|
||||
'showing_from' => $totalDomains > 0 ? $offset + 1 : 0,
|
||||
'showing_to' => min($offset + $perPage, $totalDomains)
|
||||
],
|
||||
'pagination' => $result['pagination'],
|
||||
'title' => 'Domains'
|
||||
]);
|
||||
}
|
||||
|
||||
208
app/Controllers/ErrorLogController.php
Normal file
208
app/Controllers/ErrorLogController.php
Normal file
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use Core\Controller;
|
||||
use Core\Auth;
|
||||
use App\Models\ErrorLog;
|
||||
|
||||
class ErrorLogController extends Controller
|
||||
{
|
||||
private $errorLogModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
Auth::requireAdmin();
|
||||
$this->errorLogModel = new ErrorLog();
|
||||
}
|
||||
|
||||
/**
|
||||
* Display list of errors with filters
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
// Get filters from query params
|
||||
$filters = [
|
||||
'resolved' => $_GET['resolved'] ?? '',
|
||||
'type' => $_GET['type'] ?? '',
|
||||
'sort' => $_GET['sort'] ?? 'last_occurred_at',
|
||||
'order' => $_GET['order'] ?? 'desc'
|
||||
];
|
||||
|
||||
// Pagination
|
||||
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||
$perPage = isset($_GET['per_page']) ? (int)$_GET['per_page'] : 25;
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
// Get total count using model
|
||||
$totalErrors = $this->errorLogModel->countUniqueErrors($filters);
|
||||
|
||||
// Get paginated errors using model
|
||||
$errors = $this->errorLogModel->getPaginatedErrors($filters, $perPage, $offset);
|
||||
|
||||
// Get statistics using model
|
||||
$stats = $this->errorLogModel->getAdminStats();
|
||||
|
||||
// Pagination data
|
||||
$totalPages = ceil($totalErrors / $perPage);
|
||||
$pagination = [
|
||||
'current_page' => $page,
|
||||
'total_pages' => $totalPages,
|
||||
'per_page' => $perPage,
|
||||
'total' => $totalErrors,
|
||||
'showing_from' => $totalErrors > 0 ? $offset + 1 : 0,
|
||||
'showing_to' => min($offset + $perPage, $totalErrors)
|
||||
];
|
||||
|
||||
$this->view('errors/admin-index', compact('errors', 'stats', 'filters', 'pagination'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error details
|
||||
*/
|
||||
public function show($params = [])
|
||||
{
|
||||
$errorId = $params['id'] ?? '';
|
||||
|
||||
// Get all occurrences using model
|
||||
$errorOccurrences = $this->errorLogModel->getOccurrencesByErrorId($errorId);
|
||||
|
||||
if (empty($errorOccurrences)) {
|
||||
$_SESSION['error'] = 'Error not found';
|
||||
header('Location: /errors');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get the most recent occurrence for display
|
||||
$error = $errorOccurrences[0];
|
||||
|
||||
// Parse JSON fields
|
||||
$error['stack_trace_array'] = json_decode($error['stack_trace'], true) ?? [];
|
||||
$error['request_data'] = json_decode($error['request_data'], true) ?? [];
|
||||
$error['session_data'] = json_decode($error['session_data'], true) ?? [];
|
||||
|
||||
$this->view('errors/admin-detail', compact('error', 'errorOccurrences'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark error as resolved
|
||||
*/
|
||||
public function markResolved($params = [])
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: /errors');
|
||||
exit;
|
||||
}
|
||||
|
||||
$this->verifyCsrf('/errors');
|
||||
|
||||
$errorId = $params['id'] ?? '';
|
||||
$notes = $_POST['notes'] ?? null;
|
||||
|
||||
// Mark error as resolved using model
|
||||
$this->errorLogModel->markErrorResolved($errorId, $_SESSION['user_id'], $notes);
|
||||
|
||||
$_SESSION['success'] = 'Error marked as resolved';
|
||||
header('Location: /errors/' . urlencode($errorId));
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark error as unresolved
|
||||
*/
|
||||
public function markUnresolved($params = [])
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: /errors');
|
||||
exit;
|
||||
}
|
||||
|
||||
$this->verifyCsrf('/errors');
|
||||
|
||||
$errorId = $params['id'] ?? '';
|
||||
|
||||
// Mark error as unresolved using model
|
||||
$this->errorLogModel->markErrorUnresolved($errorId);
|
||||
|
||||
$_SESSION['success'] = 'Error marked as unresolved';
|
||||
header('Location: /errors/' . urlencode($errorId));
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete error and all its occurrences
|
||||
*/
|
||||
public function delete($params = [])
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: /errors');
|
||||
exit;
|
||||
}
|
||||
|
||||
$this->verifyCsrf('/errors');
|
||||
|
||||
$errorId = $params['id'] ?? '';
|
||||
|
||||
// Delete error using model
|
||||
$this->errorLogModel->deleteByErrorId($errorId);
|
||||
|
||||
$_SESSION['success'] = 'Error deleted successfully';
|
||||
header('Location: /errors');
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear old resolved errors
|
||||
*/
|
||||
public function clearResolved()
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: /errors');
|
||||
exit;
|
||||
}
|
||||
|
||||
$this->verifyCsrf('/errors');
|
||||
|
||||
$daysOld = isset($_POST['days']) ? (int)$_POST['days'] : 30;
|
||||
|
||||
// Clear old errors using model
|
||||
$deletedCount = $this->errorLogModel->clearOldResolved($daysOld);
|
||||
|
||||
$_SESSION['success'] = "Deleted $deletedCount resolved error(s) older than $daysOld days";
|
||||
header('Location: /errors');
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk delete errors
|
||||
*/
|
||||
public function bulkDelete()
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: /errors');
|
||||
exit;
|
||||
}
|
||||
|
||||
$this->verifyCsrf('/errors');
|
||||
|
||||
$errorIdsJson = $_POST['error_ids'] ?? '[]';
|
||||
$errorIds = json_decode($errorIdsJson, true);
|
||||
|
||||
if (empty($errorIds) || !is_array($errorIds)) {
|
||||
$_SESSION['error'] = 'No errors selected for deletion';
|
||||
header('Location: /errors');
|
||||
exit;
|
||||
}
|
||||
|
||||
$deletedCount = 0;
|
||||
foreach ($errorIds as $errorId) {
|
||||
if ($this->errorLogModel->deleteByErrorId($errorId)) {
|
||||
$deletedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
$_SESSION['success'] = "Successfully deleted $deletedCount error(s)";
|
||||
header('Location: /errors');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,7 @@ class InstallerController extends Controller
|
||||
'012_link_remember_tokens_to_sessions.sql',
|
||||
'013_create_user_notifications_table.sql',
|
||||
'014_add_captcha_settings.sql',
|
||||
'015_create_error_logs_table.sql',
|
||||
];
|
||||
|
||||
try {
|
||||
@@ -105,7 +106,8 @@ class InstallerController extends Controller
|
||||
'011_create_sessions_table.sql',
|
||||
'012_link_remember_tokens_to_sessions.sql',
|
||||
'013_create_user_notifications_table.sql',
|
||||
'014_add_captcha_settings.sql'
|
||||
'014_add_captcha_settings.sql',
|
||||
'015_create_error_logs_table.sql'
|
||||
];
|
||||
}
|
||||
|
||||
@@ -261,7 +263,8 @@ class InstallerController extends Controller
|
||||
'011_create_sessions_table.sql',
|
||||
'012_link_remember_tokens_to_sessions.sql',
|
||||
'013_create_user_notifications_table.sql',
|
||||
'014_add_captcha_settings.sql'
|
||||
'014_add_captcha_settings.sql',
|
||||
'015_create_error_logs_table.sql'
|
||||
];
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO migrations (migration) VALUES (?) ON DUPLICATE KEY UPDATE migration=migration");
|
||||
|
||||
@@ -245,5 +245,41 @@ class NotificationGroupController extends Controller
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk delete notification groups
|
||||
*/
|
||||
public function bulkDelete()
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$this->redirect('/groups');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->verifyCsrf('/groups');
|
||||
|
||||
$groupIdsJson = $_POST['group_ids'] ?? '[]';
|
||||
$groupIds = json_decode($groupIdsJson, true);
|
||||
|
||||
if (empty($groupIds) || !is_array($groupIds)) {
|
||||
$_SESSION['error'] = 'No groups selected for deletion';
|
||||
$this->redirect('/groups');
|
||||
return;
|
||||
}
|
||||
|
||||
$deletedCount = 0;
|
||||
|
||||
foreach ($groupIds as $groupId) {
|
||||
try {
|
||||
$this->groupModel->deleteWithRelations((int)$groupId);
|
||||
$deletedCount++;
|
||||
} catch (\Exception $e) {
|
||||
// Continue with next group
|
||||
}
|
||||
}
|
||||
|
||||
$_SESSION['success'] = "Successfully deleted $deletedCount notification group(s)";
|
||||
$this->redirect('/groups');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Controllers;
|
||||
|
||||
use Core\Controller;
|
||||
use Core\Auth;
|
||||
use App\Models\Setting;
|
||||
|
||||
class SettingsController extends Controller
|
||||
@@ -11,14 +12,8 @@ class SettingsController extends Controller
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
Auth::requireAdmin();
|
||||
$this->settingModel = new Setting();
|
||||
|
||||
// Ensure only admins can access settings
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
$_SESSION['error'] = 'Access denied. Admin privileges required.';
|
||||
$this->redirect('/');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
public function index()
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Controllers;
|
||||
|
||||
use Core\Controller;
|
||||
use Core\Auth;
|
||||
use App\Models\TldRegistry;
|
||||
use App\Models\TldImportLog;
|
||||
use App\Services\TldRegistryService;
|
||||
@@ -22,19 +23,6 @@ class TldRegistryController extends Controller
|
||||
$this->tldService = new TldRegistryService();
|
||||
$this->logger = new Logger('tld_registry_controller');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current user is admin
|
||||
*/
|
||||
private function requireAdmin()
|
||||
{
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
$_SESSION['error'] = 'Access denied. Admin privileges required.';
|
||||
$this->redirect('/tld-registry');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display TLD registry dashboard
|
||||
*/
|
||||
@@ -91,7 +79,7 @@ class TldRegistryController extends Controller
|
||||
*/
|
||||
public function importTldList()
|
||||
{
|
||||
$this->requireAdmin();
|
||||
Auth::requireAdmin();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$this->redirect('/tld-registry');
|
||||
@@ -129,7 +117,7 @@ class TldRegistryController extends Controller
|
||||
*/
|
||||
public function importRdap()
|
||||
{
|
||||
$this->requireAdmin();
|
||||
Auth::requireAdmin();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$this->redirect('/tld-registry');
|
||||
@@ -167,7 +155,7 @@ class TldRegistryController extends Controller
|
||||
*/
|
||||
public function importWhois()
|
||||
{
|
||||
$this->requireAdmin();
|
||||
Auth::requireAdmin();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$this->redirect('/tld-registry');
|
||||
@@ -209,7 +197,7 @@ class TldRegistryController extends Controller
|
||||
*/
|
||||
public function checkUpdates()
|
||||
{
|
||||
$this->requireAdmin();
|
||||
Auth::requireAdmin();
|
||||
|
||||
try {
|
||||
$updateInfo = $this->tldService->checkForUpdates();
|
||||
@@ -251,7 +239,7 @@ class TldRegistryController extends Controller
|
||||
*/
|
||||
public function startProgressiveImport()
|
||||
{
|
||||
$this->requireAdmin();
|
||||
Auth::requireAdmin();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$this->redirect('/tld-registry');
|
||||
@@ -464,7 +452,7 @@ class TldRegistryController extends Controller
|
||||
*/
|
||||
public function bulkDelete()
|
||||
{
|
||||
$this->requireAdmin();
|
||||
Auth::requireAdmin();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$this->redirect('/tld-registry');
|
||||
@@ -512,7 +500,7 @@ class TldRegistryController extends Controller
|
||||
*/
|
||||
public function toggleActive($params = [])
|
||||
{
|
||||
$this->requireAdmin();
|
||||
Auth::requireAdmin();
|
||||
|
||||
$id = $params['id'] ?? 0;
|
||||
$tld = $this->tldModel->find($id);
|
||||
@@ -536,7 +524,7 @@ class TldRegistryController extends Controller
|
||||
*/
|
||||
public function refresh($params = [])
|
||||
{
|
||||
$this->requireAdmin();
|
||||
Auth::requireAdmin();
|
||||
|
||||
$id = $params['id'] ?? 0;
|
||||
$tld = $this->tldModel->find($id);
|
||||
|
||||
@@ -12,14 +12,8 @@ class UserController extends Controller
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
Auth::requireAdmin();
|
||||
$this->userModel = new User();
|
||||
|
||||
// Ensure only admins can access user management
|
||||
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
|
||||
$_SESSION['error'] = 'Access denied. Admin privileges required.';
|
||||
$this->redirect('/');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -378,5 +372,112 @@ class UserController extends Controller
|
||||
$this->redirect('/users');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk toggle user status (activate or deactivate)
|
||||
*/
|
||||
public function bulkToggleStatus()
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$this->redirect('/users');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->verifyCsrf('/users');
|
||||
|
||||
$userIdsJson = $_POST['user_ids'] ?? '[]';
|
||||
$userIds = json_decode($userIdsJson, true);
|
||||
$action = $_POST['action'] ?? 'activate'; // 'activate' or 'deactivate'
|
||||
|
||||
if (empty($userIds) || !is_array($userIds)) {
|
||||
$_SESSION['error'] = 'No users selected';
|
||||
$this->redirect('/users');
|
||||
return;
|
||||
}
|
||||
|
||||
$newStatus = ($action === 'activate') ? 1 : 0;
|
||||
$updatedCount = 0;
|
||||
$skippedSelf = false;
|
||||
|
||||
foreach ($userIds as $userId) {
|
||||
// Prevent modifying your own account
|
||||
if ($userId == Auth::id()) {
|
||||
$skippedSelf = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->userModel->update((int)$userId, ['is_active' => $newStatus]);
|
||||
$updatedCount++;
|
||||
} catch (\Exception $e) {
|
||||
// Continue with next user
|
||||
}
|
||||
}
|
||||
|
||||
$message = $action === 'activate' ? "Activated $updatedCount user(s)" : "Deactivated $updatedCount user(s)";
|
||||
if ($skippedSelf) {
|
||||
$message .= ' (skipped your own account)';
|
||||
}
|
||||
|
||||
$_SESSION['success'] = $message;
|
||||
$this->redirect('/users');
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk delete users
|
||||
*/
|
||||
public function bulkDelete()
|
||||
{
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$this->redirect('/users');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->verifyCsrf('/users');
|
||||
|
||||
$userIdsJson = $_POST['user_ids'] ?? '[]';
|
||||
$userIds = json_decode($userIdsJson, true);
|
||||
|
||||
if (empty($userIds) || !is_array($userIds)) {
|
||||
$_SESSION['error'] = 'No users selected for deletion';
|
||||
$this->redirect('/users');
|
||||
return;
|
||||
}
|
||||
|
||||
$deletedCount = 0;
|
||||
$skippedSelf = false;
|
||||
|
||||
foreach ($userIds as $userId) {
|
||||
// Prevent deleting your own account
|
||||
if ($userId == Auth::id()) {
|
||||
$skippedSelf = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Prevent deleting if this is the last admin
|
||||
$user = $this->userModel->find((int)$userId);
|
||||
if ($user && $user['role'] === 'admin') {
|
||||
$allAdmins = $this->userModel->where('role', 'admin');
|
||||
if (count($allAdmins) <= 1) {
|
||||
continue; // Skip - can't delete last admin
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$this->userModel->delete((int)$userId);
|
||||
$deletedCount++;
|
||||
} catch (\Exception $e) {
|
||||
// Continue with next user
|
||||
}
|
||||
}
|
||||
|
||||
$message = "Successfully deleted $deletedCount user(s)";
|
||||
if ($skippedSelf) {
|
||||
$message .= ' (skipped your own account)';
|
||||
}
|
||||
|
||||
$_SESSION['success'] = $message;
|
||||
$this->redirect('/users');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user